CSS for the Artful Microbiologist

A modern beginner tutorial for someone who likes painting, careful observation, HTML structure, and web pages that feel composed rather than assembled from random pieces.

CSS is not just "making things pretty." CSS is composition. It controls rhythm, spacing, contrast, hierarchy, motion, and how a design adapts when the frame changes.

HTML says what each thing is. CSS says how those things should appear, relate, breathe, move, and respond.

You do not need to be a math person to learn CSS. Most CSS is visual decision-making: this edge needs more space, this label needs more contrast, this card should become two columns only when it has room, this button should feel touchable, this motion should be gentle.

CSS is closer to arranging a sketchbook page than solving an algebra exam.


0. The HTML-First Rule

CSS styles meaning. It should not be used to replace meaning.

Start with good HTML:

<article class="sample-card">
  <h2>Sample A17</h2>
  <p>Cream colonies with violet shadows.</p>
</article>

Then style the class:

.sample-card {
  border: 1px solid var(--line);
  border-radius: 1.25rem;
  padding: 1rem;
  background: var(--paper);
}

Avoid writing HTML like this:

<div class="big-title">Sample A17</div>
<div class="normal-text">Cream colonies with violet shadows.</div>

CSS can make anything look like a heading, but only HTML can make it be a heading.

The best frontend work has a clean division:


1. The Shape of a CSS Rule

A CSS rule has a selector and declarations.

.sample-card {
  color: #2b211d;
  background: #fff8ec;
  border-radius: 1rem;
}

The selector chooses what to style. The declarations say which properties should change.

Read it like this:

Find every element with class sample-card. Give it this text color, this background, and this corner shape.

A CSS declaration is a small visual decision.

p {
  line-height: 1.6;
}

This says paragraphs should have breathing room between lines. It is not math in the scary sense. It is rhythm.


2. Selectors: Choosing Without Grabbing Too Hard

Start with simple selectors.

h1 {
  font-size: clamp(2.4rem, 8vw, 6rem);
}

.sample-card {
  padding: 1rem;
}

.sample-card h2 {
  margin-block-start: 0;
}

A type selector like h1 selects all h1 elements. A class selector like .sample-card selects elements with that class. A descendant selector like .sample-card h2 selects h2 elements inside sample cards.

Use classes for reusable components. They are stable and readable.

<article class="sample-card">...</article>
.sample-card {
  display: grid;
  gap: 0.75rem;
}

Avoid selectors that are too specific too early.

main section article.sample-card div.wrapper h2.title {
  color: purple;
}

That selector is like gripping a paintbrush too tightly. It works, but it makes later changes unpleasant.

Prefer simple names that describe the component.

.card-title {
  color: var(--violet);
}

3. The Cascade: Who Wins?

CSS means Cascading Style Sheets. When multiple rules apply to the same element, the browser decides which value wins.

The useful beginner version:

  1. More important origins and layers can win.
  2. More specific selectors can win.
  3. If specificity ties, the later rule wins.
p {
  color: brown;
}

.note {
  color: purple;
}
<p class="note">This note will be purple.</p>

The class selector is more specific than the type selector.

Do not fight the cascade. Organize it.

Modern CSS gives us cascade layers.

@layer reset, base, layout, components, utilities;

@layer base {
  body {
    font-family: Georgia, serif;
    line-height: 1.6;
  }
}

@layer components {
  .sample-card {
    border: 1px solid var(--line);
    border-radius: 1rem;
  }
}

Layers help you decide broad order on purpose. They are not mandatory for tiny pages, but they are useful once a stylesheet grows.


4. Custom Properties: Your Palette Labels

Custom properties are CSS variables. They let you name reusable values.

:root {
  --paper: #fff8ec;
  --ink: #2b211d;
  --violet: #6f55ff;
  --line: #dfcdbb;
}

body {
  color: var(--ink);
  background: var(--paper);
}

button {
  color: white;
  background: var(--violet);
}

Think of them like labels on paint tubes. Instead of remembering a hex code, remember the role of the color.

Custom properties can also be scoped to components.

.sample-card {
  --card-accent: #6f55ff;
  border-color: var(--card-accent);
}

.sample-card[data-mood="calm"] {
  --card-accent: #547d61;
}

.sample-card[data-mood="dramatic"] {
  --card-accent: #c6547f;
}

Now one card system can change mood without rewriting the component.


5. Color: Use Contrast, Not Just Taste

Color is emotional, but it must also be readable.

Modern CSS supports perceptual color spaces like oklch(), which can be pleasant for building palettes.

:root {
  --rose: oklch(58% 0.16 350);
  --moss: oklch(52% 0.1 145);
  --cream: oklch(97% 0.04 88);
  --ink: oklch(20% 0.03 55);
}

You can also mix colors directly in CSS.

.sample-card {
  --accent: oklch(58% 0.16 350);
  background: color-mix(
    in oklab,
    var(--accent),
    white 86%
  );
  border-color: color-mix(
    in oklab,
    var(--accent),
    black 14%
  );
}

This is useful for soft tints, borders, and hover states.

The practical rule: beautiful colors still need enough contrast. Pale pink text on cream paper may look delicate, but it may also be unreadable.

Let decoration support reading, not compete with it.


6. The Box Model: Every Element Has a Body

Most visual layout starts with the box model.

* {
  box-sizing: border-box;
}

.sample-card {
  inline-size: min(100%, 32rem);
  padding: 1rem;
  border: 1px solid var(--line);
  margin-block: 1rem;
}

The content is the inner area. Padding is space inside the border. Border is the edge. Margin is space outside.

Use logical properties when possible:

.sample-card {
  padding-block: 1rem;
  padding-inline: 1.25rem;
  margin-block-end: 1rem;
}

padding-inline follows the writing direction. In English it means left and right. In other writing modes, it adapts. Modern CSS tries to describe layout more intelligently than old left/right-only thinking.


7. Typography: Rhythm Before Decoration

Typography can make a page feel calm, joyful, serious, playful, or exhausting.

Start with readable defaults.

body {
  font-family: Charter, Georgia, serif;
  font-size: clamp(1rem, 0.35vw + 0.95rem, 1.15rem);
  line-height: 1.6;
}

h1,
h2,
h3 {
  font-family: Avenir Next, Segoe UI, sans-serif;
  line-height: 1.1;
}

clamp() lets text scale within limits. The first value is the minimum. The last value is the maximum. The middle value is the flexible part.

Use max-inline-size to stop text from becoming too wide.

.article-body {
  max-inline-size: 68ch;
}

ch roughly follows the width of the 0 character in the current font. Around 60 to 75 characters can be comfortable for long reading.

Use modern wrapping for short headings.

.hero-title {
  text-wrap: balance;
}

Balanced wrapping is useful for headings and short display text. For long paragraphs, normal wrapping usually feels more natural.


8. Flexbox: Arrange Things in One Direction

Use flexbox when items need to flow in a row or column.

.nav-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
}

Flexbox is excellent for navigation, button groups, small toolbars, and aligning items inside a card.

.sample-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.sample-meta span {
  border-radius: 999px;
  padding: 0.25rem 0.6rem;
  background: var(--tint);
}

Think of flexbox as arranging beads on a string. The string may wrap, but the main idea is still one direction.


9. Grid: Arrange Things in Two Directions

Use grid when rows and columns matter.

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
}

This creates as many columns as can fit, while each card gets at least 16rem.

Grid is excellent for galleries, dashboards, page regions, and cards with internal structure.

.sample-card {
  display: grid;
  gap: 0.85rem;
}

.sample-card header {
  display: grid;
  gap: 0.25rem;
}

You do not need to choose flexbox or grid forever. Use the one that fits the local problem.

Flexbox is a row of paint pans. Grid is a specimen tray.


10. Container Queries: Respond to the Frame

Media queries respond to the viewport. Container queries respond to the space a component actually has.

First, mark a parent frame as a container.

<div class="sample-frame">
  <article class="sample-card">
    <h2>Sample A17</h2>
    <p>Cream colonies with violet shadows.</p>
  </article>
</div>
.sample-frame {
  container-type: inline-size;
}

.sample-card {
  display: grid;
  gap: 0.85rem;
}

Then write a query for the space available inside that frame.

@container (min-width: 34rem) {
  .sample-card {
    grid-template-columns: 12rem 1fr;
    align-items: start;
  }
}

Container queries style descendants of the container. That is why the frame is the container and the card inside it changes.

This is powerful because the same card can be narrow in a sidebar and wide in a gallery, even on the same screen.

Container queries are a modern way to make components more self-aware without asking JavaScript to measure them.


11. State Without JavaScript

Many visual states do not need JavaScript.

a:hover {
  text-decoration-thickness: 0.14em;
}

button:focus-visible {
  outline: 3px solid var(--violet);
  outline-offset: 3px;
}

input:invalid {
  border-color: var(--rose);
}

details[open] summary {
  color: var(--violet);
}

CSS can also style a parent based on what it contains with :has().

.field:has(input:invalid) {
  background: color-mix(
    in oklab,
    var(--rose),
    white 88%
  );
}

.sample-card:has(input:checked) {
  border-color: var(--violet);
  box-shadow: 0 0 0 4px var(--violet-soft);
}

This is one of the biggest modern CSS upgrades. It reduces many small JavaScript tricks.

Use JavaScript when the state must be saved, calculated, fetched, or shared with other parts of an app. Use CSS when the state is already visible in the HTML.


12. Forms Can Be Styled Gently

HTML gives forms meaning and validation. CSS makes them pleasant to use.

.field {
  display: grid;
  gap: 0.35rem;
}

.field label {
  font-weight: 700;
}

input,
select,
textarea {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  padding: 0.7rem 0.8rem;
  color: var(--ink);
  background: white;
  font: inherit;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  border-color: var(--violet);
  outline: 3px solid var(--violet-soft);
}

You can style checkboxes and radio buttons with accent-color.

input[type="checkbox"],
input[type="radio"] {
  accent-color: var(--violet);
}

Do not remove focus styles unless you replace them with something clear. Keyboard users need to see where they are.


13. Motion: A Small Amount Is Enough

Motion should guide attention, not perform acrobatics.

.soft-button {
  transform: translateY(0);
  transition:
    transform 180ms ease,
    box-shadow 180ms ease,
    background-color 180ms ease;
}

.soft-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 0.8rem 1.8rem rgb(0 0 0 / 0.12);
}

Always respect reduced motion preferences.

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

A good transition feels like a brushstroke. It should not grab the page by the shoulders.


Modern CSS supports native nesting. This can make components easier to read.

.sample-card {
  border: 1px solid var(--line);
  border-radius: 1rem;
  padding: 1rem;

  & h2 {
    margin-block: 0;
    color: var(--violet);
  }

  & p {
    margin-block-end: 0;
  }

  &:hover {
    border-color: var(--rose);
  }
}

The & means "the selector I am currently inside." Here, & h2 means .sample-card h2.

Nesting is useful, but keep it shallow. Two levels are usually enough. Deep nesting becomes a maze.


This project uses semantic HTML plus modern CSS. It has responsive cards, custom properties, grid, a container query, :has(), native radio controls, and a no-JavaScript visual filter.

Create index.html.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Culture Gallery</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="culture-board">
    <header class="hero">
      <p class="kicker">Microbiology x sketchbook</p>
      <h1>Culture Gallery</h1>
      <p>
        Filter small visual studies by mood. No JavaScript required for this
        version; CSS reads the checked radio button.
      </p>
    </header>

    <form class="filters" aria-label="Filter culture cards">
      <fieldset>
        <legend>Choose a mood</legend>

        <label>
          <input type="radio" name="mood" value="all" checked>
          All
        </label>

        <label>
          <input type="radio" name="mood" value="calm">
          Calm
        </label>

        <label>
          <input type="radio" name="mood" value="strange">
          Strange
        </label>

        <label>
          <input type="radio" name="mood" value="dramatic">
          Dramatic
        </label>
      </fieldset>
    </form>

    <section class="gallery" aria-label="Culture sketch cards">
      <article class="sample-card" data-mood="calm">
        <p class="tag">Calm</p>
        <h2>Blue wash</h2>
        <p>
          Pale center, soft rim, and a watery edge like diluted ink.
        </p>
      </article>

      <article class="sample-card" data-mood="strange">
        <p class="tag">Strange</p>
        <h2>Forked edge</h2>
        <p>
          Two uneven branches grew away from the main colony.
        </p>
      </article>

      <article class="sample-card" data-mood="dramatic">
        <p class="tag">Dramatic</p>
        <h2>Crimson ring</h2>
        <p>
          A dark outer halo made the center look almost luminous.
        </p>
      </article>
    </section>
  </main>
</body>
</html>

Create styles.css.

@layer base, layout, components, states;

@layer base {
  :root {
    --paper: oklch(97% 0.035 88);
    --ink: oklch(21% 0.035 55);
    --soft-ink: oklch(42% 0.04 55);
    --violet: oklch(55% 0.19 292);
    --rose: oklch(58% 0.16 350);
    --moss: oklch(52% 0.1 145);
    --line: oklch(83% 0.04 75);
    --radius: 1.4rem;
  }

  * {
    box-sizing: border-box;
  }

  body {
    margin: 0;
    color: var(--ink);
    background:
      radial-gradient(circle at 10% 10%, #ded7ff, transparent 18rem),
      radial-gradient(circle at 90% 0%, #ffd8e6, transparent 20rem),
      var(--paper);
    font-family: Charter, Georgia, serif;
    font-size: clamp(1rem, 0.35vw + 0.95rem, 1.15rem);
    line-height: 1.6;
  }

  h1,
  h2,
  p {
    margin-block-start: 0;
  }
}

@layer layout {
  .culture-board {
    width: min(68rem, calc(100% - 2rem));
    margin-inline: auto;
    padding-block: clamp(2rem, 7vw, 6rem);
  }

  .hero {
    max-inline-size: 48rem;
    margin-block-end: 1.5rem;
  }

  .gallery {
    container-type: inline-size;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: 1rem;
  }
}

@layer components {
  .kicker,
  .tag {
    color: var(--violet);
    font-family: Avenir Next, Segoe UI, sans-serif;
    font-size: 0.78rem;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
  }

  h1 {
    max-inline-size: 10ch;
    font-family: Avenir Next, Segoe UI, sans-serif;
    font-size: clamp(3rem, 12vw, 7rem);
    line-height: 0.9;
    letter-spacing: -0.08em;
    text-wrap: balance;
  }

  .filters fieldset {
    display: flex;
    flex-wrap: wrap;
    gap: 0.65rem;
    margin: 0 0 1rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.85rem;
    background: rgb(255 255 255 / 0.55);
  }

  .filters legend {
    padding-inline: 0.35rem;
    font-weight: 800;
  }

  .filters label {
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0.4rem 0.7rem;
    background: white;
    cursor: pointer;
  }

  input[type="radio"] {
    accent-color: var(--violet);
  }

  .sample-card {
    --accent: var(--violet);
    display: grid;
    gap: 0.55rem;
    min-block-size: 13rem;
    border: 1px solid color-mix(in oklab, var(--accent), black 18%);
    border-radius: var(--radius);
    padding: 1rem;
    background: color-mix(in oklab, var(--accent), white 88%);
    box-shadow: 0 1rem 2rem rgb(0 0 0 / 0.08);

    &[data-mood="calm"] {
      --accent: var(--moss);
    }

    &[data-mood="dramatic"] {
      --accent: var(--rose);
    }

    & h2 {
      font-family: Avenir Next, Segoe UI, sans-serif;
      font-size: clamp(1.35rem, 2vw + 1rem, 2.2rem);
      line-height: 1;
    }

    & p:last-child {
      color: var(--soft-ink);
    }
  }

  @container (min-width: 24rem) {
    .sample-card {
      grid-template-columns: 8rem 1fr;
      align-items: start;
    }

    .sample-card .tag {
      grid-row: span 2;
    }
  }
}

@layer states {
  .culture-board:has(input[value="calm"]:checked)
    .sample-card:not([data-mood="calm"]),
  .culture-board:has(input[value="strange"]:checked)
    .sample-card:not([data-mood="strange"]),
  .culture-board:has(input[value="dramatic"]:checked)
    .sample-card:not([data-mood="dramatic"]) {
    display: none;
  }

  .filters label:has(input:checked) {
    border-color: var(--violet);
    background: color-mix(in oklab, var(--violet), white 86%);
  }

  .sample-card:hover {
    transform: translateY(-2px);
  }

  .sample-card,
  .filters label {
    transition:
      transform 180ms ease,
      border-color 180ms ease,
      background-color 180ms ease;
  }

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      transition-duration: 0.001ms !important;
    }
  }
}

The interesting part is this:

.culture-board:has(input[value="calm"]:checked)
  .sample-card:not([data-mood="calm"]) {
  display: none;
}

CSS sees that a radio button is checked and hides cards that do not match. No JavaScript. No stored data. No app state. Just visible HTML state and a modern selector.

If you later wanted to save the chosen filter, fetch cards from a server, or count matching samples, JavaScript would earn its place.


16. CSS Checklist

Before reaching for JavaScript, ask:

CSS is art with constraints. That is not a limitation. That is the medium.


Further Reading