Sandbox

Sandbox is where i throw shit at the wall. not to see what sticks — to see what i like. no aesthetic obligation, no “does this fit the site” — just me testing technique for technique's sake. my HTML & CSS fundamentals live here too, unguided and untouched by the syllabus — and under them, my builds from The Odin Project. take it as the raw, unfiltered version of how i actually learn.
▸ HTML & CSS Fundamentals 16 builds · from scratch, no tutorial
▸ CSS Selectors
a full page — header, nav, cards, sidebar, footer — with every selector type wired into it: grouped, element, class, #id, descendant, direct child, and attribute selectors matching on href^="https", type="email" and [disabled]. the descendant and child rules paint different colors on purpose, so the gap between .card p and .card > p is something you see, not something you take on faith.
html
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>CSS Selectors Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <header>
      <h1>Selectors Playground</h1>
      <p>A tiny page for seeing CSS selectors in action.</p>
    </header>

    <nav aria-label="Main navigation">
      <a href="#articles">Articles</a> |
      <a href="https://developer.mozilla.org/">MDN reference</a>
    </nav>

    <div class="layout">
      <main id="articles">
        <h2>Featured Articles</h2>

        <section class="cards" aria-label="Article cards">
          <article class="card">
            <h3>Direct Child Paragraph</h3>
            <p>This direct child paragraph gets the green child-selector style.</p>
            <div>
              <p>This nested paragraph gets the blue descendant-selector style.</p>
            </div>
            <button id="cta">Read article</button>
          </article>

          <article class="card">
            <h3>Second Card</h3>
            <p>This is another direct child paragraph inside a card.</p>
            <button disabled>Coming soon</button>
          </article>
        </section>
      </main>

      <aside>
        <h2>Newsletter</h2>
        <p>Get one short CSS tip each week.</p>
        <label for="email">Email address</label>
        <input id="email" type="email" placeholder="you@example.com">
        <button>Subscribe</button>
      </aside>
    </div>

    <footer>
      <p>Built to explore CSS selectors.</p>
    </footer>
  </body>
</html>
css
styles.css
* {
  box-sizing: border-box;
}

body {
  max-width: 900px;
  margin: 0 auto;
  padding: 24px;
  font-family: Arial, sans-serif;
  line-height: 1.5;
  background: #f6f8fb;
  color: #1f2937;
}

header,
nav,
main,
aside,
footer {
  padding: 16px;
  border: 2px solid #cbd5e1;
  border-radius: 10px;
  background: white;
}

nav,
footer {
  margin-top: 16px;
}

.layout {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 16px;
  margin-top: 16px;
}

.cards {
  display: grid;
  gap: 12px;
}

/* Group selector: targets every h1, h2, and h3 heading. */
h1,
h2,
h3 {
  color: #3730a3;
  border-bottom: 2px solid #c4b5fd;
}

/* Element selector: targets every paragraph on the page. */
p {
  color: #0f766e;
}

/* Class selector: targets every element with class="card". */
.card {
  padding: 14px;
  border: 3px solid #f59e0b;
  border-radius: 8px;
  background: #fffbeb;
}

/* ID selector: targets the one element with id="cta". */
#cta {
  border: 3px solid #db2777;
  background: #fce7f3;
  color: #9d174d;
}

/* Descendant selector: targets paragraphs anywhere inside a .card. */
.card p {
  background: #dbeafe;
  border-left: 5px solid #2563eb;
  padding: 6px;
}

/* Child selector: targets only paragraphs directly inside a .card. */
.card > p {
  background: #dcfce7;
  border-left-color: #16a34a;
  color: #166534;
}

/* Attribute selector: targets links whose href begins with "https". */
a[href^="https"] {
  color: #7c3aed;
  font-weight: bold;
}

/* Attribute selector: targets email input fields. */
input[type="email"] {
  border: 3px solid #0891b2;
  background: #ecfeff;
  padding: 8px;
}

/* Attribute selector: targets disabled buttons. */
button[disabled] {
  border: 2px dashed #64748b;
  background: #e2e8f0;
  color: #64748b;
}

button {
  padding: 8px 12px;
  border: 2px solid #1d4ed8;
  border-radius: 6px;
  background: #2563eb;
  color: white;
}

label,
input,
button {
  display: block;
  margin-top: 8px;
}
preview
▸ CSS Specificity
three head-to-head matchups, each labeled with which rule wins and why: two .card rules at identical specificity where the later one takes it, p losing to .message, and .button-style losing to #cta. the winning value is the one on screen, so the cascade is legible without opening devtools.
html
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Specificity Mini Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <article class="card">
      <h1>Specificity</h1>
      <p class="message">This paragraph is green because the class rule wins.</p>
      <button id="cta" class="button-style" type="button">This button is pink</button>
    </article>
  </body>
</html>
css
styles.css
body {
  font-family: Arial, sans-serif;
  margin: 40px;
}

.card {
  max-width: 360px;
  padding: 20px;
  border: 4px solid #2563eb; /* Loses: the next .card rule has the same specificity. */
}

.card {
  border-color: #f97316; /* Wins: same specificity, but this rule comes later. */
}

p {
  color: #2563eb; /* Loses: an element selector is less specific than a class selector. */
}

.message {
  color: #16a34a; /* Wins: a class selector is more specific than p. */
}

.button-style {
  background: #7c3aed; /* Loses: an ID selector is more specific than a class selector. */
}

#cta {
  background: #db2777; /* Wins: an ID selector is more specific than a class selector. */
}

button {
  padding: 10px 14px;
  border: 0;
  border-radius: 6px;
  color: white;
}
preview
▸ CSS Inheritance
which properties come down the tree and which don't — color and font-family inherit into the heading and paragraph automatically, background-color doesn't. then the three override keywords in practice: inherit pulling the card's color into a button that would otherwise use the browser default, initial resetting a strong back to spec, and unset letting font-family fall back to inheriting.
html
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Inheritance Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <article class="card">
      <h1>Inheritance Playground</h1>
      <p>The card's color and font are inherited by this paragraph, but <strong>this strong text resets its color.</strong></p>
      <button type="button">Inherited button color</button>
    </article>
  </body>
</html>
css
styles.css
/* color and font-family inherit by default, so the heading and paragraph use these values; background-color does not inherit, so children stay transparent. */
.card {
  max-width: 420px;
  margin: 40px auto;
  padding: 24px;
  border: 3px solid #7c3aed;
  border-radius: 12px;
  background-color: #f3e8ff;
  color: #5b21b6;
  font-family: Georgia, serif;
}

/* inherit explicitly makes the button take the card's text color instead of its browser-default button color. */
button {
  padding: 10px 14px;
  border: 2px solid currentColor;
  border-radius: 6px;
  background: #fef3c7;
  color: inherit;
}

/* initial resets color to its CSS initial value, so this strong text does not use the card's purple color. */
strong {
  color: initial;
}

/* unset checks the property's normal behavior: font-family inherits, so the button receives the card's Georgia font. */
button {
  font-family: unset;
}
preview
▸ CSS Pseudo-classes
:nth-child(2) and p:nth-of-type(2) applied to the same container of mixed elements — both selectors say "2", and they highlight different things. one counts every sibling, the other counts only siblings of its own type. the matched elements are color-coded and the markup labels its own positions, so the miscount you'd otherwise make is visible instead of theoretical.
html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pseudo-class Playground</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="container">
    <p>First element: first paragraph</p>
    <div>Second element: first div — matched by :nth-child(2)</div>
    <span>Third element: span</span>
    <p>Fourth element: second paragraph — matched by p:nth-of-type(2)</p>
    <div>Fifth element: second div</div>
  </main>
</body>
</html>
css
styles.css
.container {
  max-width: 560px;
  margin: 3rem auto;
  padding: 1.5rem;
  border: 2px solid #334155;
  border-radius: 12px;
  font-family: system-ui, sans-serif;
}

.container > * {
  display: block;
  margin: 0.75rem 0;
  padding: 0.75rem;
  border: 1px solid #cbd5e1;
  border-radius: 6px;
}

/* Selects the second child overall, no matter what kind of element it is. */
.container > :nth-child(2) {
  background-color: #fde68a;
  border-color: #d97706;
}

/* Selects the second <p> among its sibling <p> elements. */
.container > p:nth-of-type(2) {
  background-color: #bfdbfe;
  border-color: #2563eb;
}

/*
  Both selectors use "2", but they count different things:
  :nth-child(2) counts every child, so it highlights the second element overall
  (the first <div>). p:nth-of-type(2) counts only <p> siblings, so it highlights
  the second <p>, which is the fourth element overall.
*/
preview
▸ CSS Pseudo-elements
two paragraphs styled identically — same background, padding, border on both ::before rules — except one declares content and one doesn't. only one renders. content isn't decoration, it's the switch that generates the pseudo-element at all; without it there's nothing for the other properties to decorate.
html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pseudo-element Playground</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="container">
    <p class="with-content">Pseudo-element playground paragraph.</p>
    <p class="without-content">Pseudo-element playground paragraph.</p>
  </main>
</body>
</html>
css
styles.css
/* Keeps the example centered and easy to compare. */
.container {
  max-width: 560px;
  margin: 3rem auto;
  font-family: system-ui, sans-serif;
}

/* Gives both paragraphs the same appearance. */
p {
  padding: 1rem;
  border: 1px solid #94a3b8;
  border-radius: 6px;
}

/* content generates this ::before box and places "NEW" inside it. */
.with-content::before {
  content: "NEW";
  background-color: #fde68a;
  padding: 0.25rem 0.5rem;
  border: 1px solid #d97706;
  margin-right: 0.75rem;
}

/*
  Without content, the browser does not generate this ::before box at all.
  content is not just its text; it tells the browser to create the
  pseudo-element, so the other styles have nothing to decorate.
*/
.without-content::before {
  background-color: #fde68a;
  padding: 0.25rem 0.5rem;
  border: 1px solid #d97706;
  margin-right: 0.75rem;
}
preview
▸ CSS Box Model
a labeled cutaway of the box model — margin, border, padding and content each rendered as its own colored layer, nested the way the model actually stacks. below it, two boxes both declared width: 200px: content-box renders at 260px, border-box renders at 200px. same number in, different number out. TRBL shorthand order explained up top.
html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Box Model Playground</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main>
    <h1>CSS Box Model Playground</h1>
    <p class="intro">The pink space around the orange border is the element's actual margin.</p>

    <section class="box-stage" aria-label="A labeled CSS box model example">
      <span class="margin-label">margin</span>
      <div class="border-layer">
        <span class="border-label">border</span>
        <div class="padding-layer">
          <span class="padding-label">padding</span>
          <div class="content-layer"><span>content</span></div>
        </div>
      </div>
    </section>

    <section aria-labelledby="sizing-heading">
      <h2 id="sizing-heading">Same declared width, different rendered widths</h2>
      <div class="comparison">
        <div class="sizing-box content-box">
          <strong>content-box</strong>
          <span>Declared: 200px</span>
          <span>Rendered: 260px</span>
        </div>
        <div class="sizing-box border-box">
          <strong>border-box</strong>
          <span>Declared: 200px</span>
          <span>Rendered: 200px</span>
        </div>
      </div>
    </section>
  </main>
</body>
</html>
css
styles.css
/*
  TRBL is the order used by four-value shorthand properties:
  top, right, bottom, left (clockwise from the top).
  Example: margin: 10px 20px 30px 40px;
*/

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  padding: 48px 24px;
  background: #f8fafc;
  color: #172033;
  font-family: Arial, sans-serif;
}

main {
  width: min(100%, 760px);
  margin: 0 auto;
}

h1 {
  margin: 0 0 8px;
}

.intro {
  margin: 0 0 32px;
  color: #475569;
}

.box-stage {
  position: relative;
  display: flow-root;
  margin-bottom: 48px;
  padding: 1px;
  background: #ffd6e5;
  outline: 3px dashed #e11d78;
  outline-offset: 4px;
}

.margin-label {
  position: absolute;
  top: 10px;
  left: 12px;
  padding: 3px 7px;
  background: #e11d78;
  color: #ffffff;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.border-layer {
  position: relative;
  margin: 58px;
  border: 14px solid #f97316;
  background: #fed7aa;
  outline: 2px solid #c2410c;
}

.border-label {
  position: absolute;
  top: -10px;
  left: 10px;
  padding: 2px 6px;
  background: #c2410c;
  color: #ffffff;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
}

.padding-layer {
  position: relative;
  padding: 52px;
  background: #bfdbfe;
  outline: 2px solid #2563eb;
}

.padding-label {
  position: absolute;
  top: 12px;
  left: 12px;
  padding: 2px 6px;
  background: #2563eb;
  color: #ffffff;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
}

.content-layer {
  display: grid;
  min-height: 112px;
  place-items: center;
  background: #bbf7d0;
  outline: 2px solid #16a34a;
  color: #14532d;
  font-weight: 700;
}

.content-layer span {
  padding: 5px 9px;
  background: #16a34a;
  color: #ffffff;
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

h2 {
  margin: 0 0 16px;
  font-size: 1.25rem;
}

.comparison {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
}

.sizing-box {
  padding: 24px;
  border: 6px solid #4f46e5;
  background: #e0e7ff;
  color: #312e81;
}

/* Declared width: 200px; actual rendered width: 260px (200 + 48px padding + 12px border). */
.content-box {
  box-sizing: content-box;
  width: 200px;
}

/* Declared width: 200px; actual rendered width: 200px (padding and border are included). */
.border-box {
  box-sizing: border-box;
  width: 200px;
}

.sizing-box strong,
.sizing-box span {
  display: block;
}

.sizing-box strong {
  margin-bottom: 8px;
}

.sizing-box span {
  font-size: 0.85rem;
}
preview
▸ CSS Units
four boxes sized in px, %, rem, and calc(50% - 20px), inside a container you can drag wider and narrower by its corner — resize: horizontal, no javascript. watch which boxes track the container and which refuse to move. each box comments what it's measured relative to.
html
index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CSS Units Playground</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>CSS Units Playground</h1>
  <p class="hint">
    Resize the browser window or drag the container's bottom-right corner.
    Watch which boxes track the container and which stay fixed.
  </p>

  <main class="container">
    <!-- px is an absolute CSS unit, so this width is not relative to the parent. -->
    <div class="box box-1">Box 1 — width: 240px</div>

    <!-- % is relative to the width of this box's containing parent. -->
    <div class="box box-2">Box 2 — width: 60%</div>

    <!-- rem is relative to the root html element's font size (16px here). -->
    <div class="box box-3">Box 3 — width: 18rem</div>

    <!-- calc() combines values: 50% is relative to the parent, then 20px is subtracted. -->
    <div class="box box-4">Box 4 — width: calc(50% - 20px)</div>
  </main>
</body>
</html>
css
styles.css
:root {
  font-size: 16px;
  color-scheme: light;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  padding: 2rem;
  font-family: system-ui, sans-serif;
  color: #172033;
  background: #eef2ff;
}

h1 {
  margin-top: 0;
}

.hint {
  max-width: 65ch;
  color: #4b5563;
}

.container {
  width: min(80vw, 56rem);
  min-width: 18rem;
  max-width: 95vw;
  margin-top: 2rem;
  padding: 1.25rem;
  overflow: auto;
  resize: horizontal;
  border: 3px dashed #64748b;
  border-radius: 0.75rem;
  background: white;
  box-shadow: 0 1rem 2rem rgb(15 23 42 / 10%);
}

.box {
  margin: 0.75rem 0 1.5rem;
  padding: 1rem;
  border: 2px solid #3730a3;
  border-radius: 0.5rem;
  color: white;
  background: #4f46e5;
  white-space: nowrap;
}

.box-1 {
  width: 240px;
}

.box-2 {
  width: 60%;
}

.box-3 {
  width: 18rem;
}

.box-4 {
  width: calc(50% - 20px);
}

code {
  padding: 0.1em 0.35em;
  border-radius: 0.25rem;
  background: #e2e8f0;
}
preview
▸ CSS Sizing
the responsive-sizing toolkit in one card: % widths that flex with the parent, max-width capping how far they go, rem text that scales with the root, and a clamp(2rem, 5vw, 4rem) heading that tracks the viewport but refuses to pass its own floor or ceiling. box-sizing: border-box underneath keeping the math honest.
html
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Sizing Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <div class="wrapper">
      <article class="card">
        <h1>Sizing Playground</h1>
        <p>Resize the browser window to see percentage widths and the clamped heading size respond.</p>
        <button type="button">Try sizing</button>
      </article>
    </div>
  </body>
</html>
css
styles.css
/* border-box makes declared widths include padding and borders, which keeps sizing predictable. */
* {
  box-sizing: border-box;
}

/* A little fixed px padding gives the page consistent space around its content. */
body {
  margin: 0;
  padding: 32px;
  font-family: Arial, sans-serif;
  background: #f1f5f9;
}

/* A percentage width lets the wrapper shrink and grow with its parent, while max-width stops it becoming too wide. */
.wrapper {
  width: 90%;
  max-width: 900px;
  margin: 0 auto;
}

/* The card uses a responsive percentage width but fixed px padding and border thickness for a stable frame. */
.card {
  width: 75%;
  padding: 24px;
  border: 4px solid #2563eb;
  border-radius: 12px;
  background: white;
}

/* clamp() scales the heading with the viewport (vw) but never below 2rem or above 4rem. */
h1 {
  font-size: clamp(2rem, 5vw, 4rem);
  margin-top: 0;
  color: #1e3a8a;
}

/* rem sizes text relative to the root font size, which supports consistent and accessible text scaling. */
p {
  font-size: 1.125rem;
  line-height: 1.6;
}

/* Fixed px dimensions make this button keep a dependable tap target size. */
button {
  width: 160px;
  padding: 12px 16px;
  border: 0;
  border-radius: 6px;
  background: #0f766e;
  color: white;
  font-size: 1rem;
}
preview
▸ CSS Flexbox — Property Reference
the glossary. five sections isolating one property each — flex-direction, justify-content, align-items, flex-wrap, flex-grow — with every value rendered side by side in the same row, so all five justify-content options are one glance apart instead of five separate demos.
html
flexbox.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Flexbox Playground</title>
  <link rel="stylesheet" href="flexbox.css"> <!-- connects this file to your CSS -->
</head>

  <h2>1. Flex Direction</h2>
  <p class="label">row (default) vs column — controls which way the main axis runs</p>
  <div class="demo-row">
    <div class="box direction-row">
      <div class="item">1</div><div class="item">2</div><div class="item">3</div>
    </div>
    <div class="box direction-column">
      <div class="item">1</div><div class="item">2</div><div class="item">3</div>
    </div>
  </div>

  <h2>2. Justify Content</h2>
  <p class="label">controls spacing of items along the main axis</p>
  <div class="demo-row">
    <div class="box justify-demo jc-start"><div class="item">1</div><div class="item">2</div></div>
    <div class="box justify-demo jc-end"><div class="item">1</div><div class="item">2</div></div>
    <div class="box justify-demo jc-center"><div class="item">1</div><div class="item">2</div></div>
    <div class="box justify-demo jc-between"><div class="item">1</div><div class="item">2</div></div>
    <div class="box justify-demo jc-around"><div class="item">1</div><div class="item">2</div></div>
  </div>

  <h2>3. Align Items</h2>
  <p class="label">controls positioning along the cross axis (perpendicular to main axis)</p>
  <div class="demo-row">
    <div class="box align-demo ai-start"><div class="item">1</div><div class="item">2</div></div>
    <div class="box align-demo ai-center"><div class="item">1</div><div class="item">2</div></div>
    <div class="box align-demo ai-end"><div class="item">1</div><div class="item">2</div></div>
  </div>

  <h2>4. Flex Wrap</h2>
  <p class="label">too many items forced onto one line vs allowed to wrap</p>
  <div class="box wrap-demo">
    <div class="item">1</div><div class="item">2</div><div class="item">3</div>
    <div class="item">4</div><div class="item">5</div><div class="item">6</div>
  </div>

  <h2>5. Flex Grow</h2>
  <p class="label">item 2 has flex-grow:2, so it claims twice the leftover space of items 1 and 3</p>
  <div class="box grow-demo">
    <div class="item">1</div><div class="item">2</div><div class="item">3</div>
  </div>

</body>
</html>
css
flexbox.css
body {
  font-family: sans-serif; /* keeps labels readable */
  background-color: #1a1a1a; /* dark background so the colored boxes pop */
  color: #eee; /* light text on dark background */
  padding: 2rem; /* breathing room around the whole page */
}

h2 {
  margin-top: 2.5rem; /* space above each section title */
  margin-bottom: 0.5rem; /* tighter space below, closer to its demo */
  color: #7fd1ff; /* light blue to separate titles from body text */
}

p.label {
  font-size: 0.9rem; /* smaller than body text, it's a caption not content */
  color: #aaa; /* dimmed so it doesn't compete with the boxes */
  margin-bottom: 0.5rem; /* small gap before the demo box starts */
}

.demo-row {
  display: flex; /* lays out several mini flex-containers side by side for comparison */
  gap: 2rem; /* space between the mini-containers */
  flex-wrap: wrap; /* lets the row wrap on narrow screens instead of overflowing */
}

.box {
  border: 2px solid #444; /* outlines each flex-container so you can see its edges */
  padding: 1rem; /* space inside each flex-container, around the items */
  background-color: #222; /* slightly lighter than page background, marks the container boundary */
}

.item {
  background-color: #ff6b6b; /* red items are the ones being positioned/sized — the thing you're watching move */
  padding: 0.75rem; /* space inside each item so the number isn't cramped */
  margin: 0.25rem; /* tiny gap between items so touching edges are visible */
  color: #1a1a1a; /* dark text readable against the red background */
  font-weight: bold; /* makes the item numbers easy to scan */
  display: flex; /* used ONLY to center the number inside each item */
  align-items: center; /* centers number vertically inside the item */
  justify-content: center; /* centers number horizontally inside the item */
}

/* SECTION 1: flex-direction — controls which way the main axis runs */
.direction-row {
  display: flex; /* THE property being demoed */
  flex-direction: row; /* items line up left-to-right (default) */
  width: 200px;
  height: 120px;
}
.direction-row .item { width: 40px; height: 40px; }

.direction-column {
  display: flex; /* THE property being demoed */
  flex-direction: column; /* items stack top-to-bottom instead */
  width: 200px;
  height: 120px;
}
.direction-column .item { width: 40px; height: 40px; }

/* SECTION 2: justify-content — controls spacing along the main axis */
.justify-demo {
  display: flex; /* THE property being demoed */
  width: 260px;
  height: 80px;
}
.justify-demo .item { width: 40px; height: 40px; }
.jc-start   { justify-content: flex-start; }   /* items pushed to the start, no gap between them */
.jc-end     { justify-content: flex-end; }     /* items pushed to the end */
.jc-center  { justify-content: center; }       /* items clumped in the middle */
.jc-between { justify-content: space-between; }/* equal gaps BETWEEN items, none at the edges */
.jc-around  { justify-content: space-around; } /* equal gaps AROUND each item, so edges get half-gaps */

/* SECTION 3: align-items — controls positioning along the cross axis (perpendicular to main axis) */
.align-demo {
  display: flex; /* THE property being demoed */
  width: 220px;
  height: 100px; /* taller box so vertical alignment is actually visible */
}
.align-demo .item { width: 40px; height: 30px; } /* shorter items than the container, so alignment shows */
.ai-start  { align-items: flex-start; }  /* items stick to the top */
.ai-center { align-items: center; }      /* items centered vertically */
.ai-end    { align-items: flex-end; }    /* items stick to the bottom */

/* SECTION 4: flex-wrap — controls whether items are forced onto one line or allowed to wrap */
.wrap-demo {
  display: flex; /* THE property being demoed */
  flex-wrap: wrap; /* items that don't fit spill onto a new line instead of shrinking or overflowing */
  width: 220px; /* deliberately too narrow to fit all 6 items on one line */
}
.wrap-demo .item { width: 60px; height: 40px; }

/* SECTION 5: flex-grow — controls how items divide up leftover space */
.grow-demo {
  display: flex; /* THE property being demoed */
  width: 300px;
  height: 60px;
}
.grow-demo .item:nth-child(1) { flex-grow: 1; } /* takes 1 share of leftover space */
.grow-demo .item:nth-child(2) { flex-grow: 2; } /* takes 2 shares — ends up twice as wide as item 1 */
.grow-demo .item:nth-child(3) { flex-grow: 1; } /* takes 1 share, same as item 1 */
preview
▸ CSS Flexbox — Interactive Demo
the applied version. four cards in a flex row you can squeeze and stretch by dragging the container's edge — flex-wrap kicking items onto a new row when they run out of space, and one card running flex: 1 so it swallows whatever's left. the reference shows you the properties; this one lets you push on them.
html
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flexbox Playground — Interactive Demo</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <main class="playground">
      <h1>Flexbox Playground — Interactive Demo</h1>
      <p><strong>Drag the corner</strong> of the dashed box below to resize it. Watch two things: the cards <strong>wrap</strong> onto a new row once they run out of space, and the green card (<code>flex: 1</code>) <strong>grows</strong> to fill whatever room is left over.</p>

      <div class="resize-wrap">
        <section class="card-container" aria-label="Four flexbox cards">
          <article class="card">
            <h2>Card One</h2>
            <p>This card has a preferred width of 180 pixels.</p>
          </article>

          <article class="card grow">
            <h2>Card Two</h2>
            <p>This card grows into the available space with flex: 1.</p>
          </article>

          <article class="card">
            <h2>Card Three</h2>
            <p>This card can wrap to a new line on a smaller screen.</p>
          </article>

          <article class="card">
            <h2>Card Four</h2>
            <p>This card is another child of the same flex container.</p>
          </article>
        </section>
      </div>
    </main>
  </body>
</html>
css
styles.css
/* Adds comfortable page spacing so the layout is easier to inspect. */
body {
  margin: 0;
  padding: 2rem;
  font-family: Arial, sans-serif;
  background: #f1f5f9;
  color: #1f2937;
}

/* Limits the page width so the flex layout stays readable on wide screens. */
.playground {
  max-width: 900px;
  margin: 0 auto;
}

/* Makes the container itself draggable by its bottom-right corner, so the
   flex behavior can be tested without resizing the whole browser window. */
.resize-wrap {
  resize: horizontal;
  overflow: auto;
  width: 100%;
  max-width: 100%;
  min-width: 260px;
  padding: 1rem;
  border: 3px dashed #94a3b8;
  border-radius: 0.75rem;
  background: #e2e8f0;
}

/* Turns the parent into a flex container so its cards can be laid out together. */
.card-container {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: stretch;
  flex-wrap: wrap;
  gap: 1rem;
  padding: 1rem;
  border: 3px solid #2563eb;
  border-radius: 0.75rem;
  background: white;
}

/* Gives each card a useful starting size while allowing it to shrink if needed. */
.card {
  flex: 0 1 180px;
  min-height: 130px;
  padding: 1rem;
  border: 2px solid #93c5fd;
  border-radius: 0.5rem;
  background: #dbeafe;
}

/* Lets this card use remaining row space, demonstrating flex: 1. */
.card.grow {
  flex: 1;
  background: #dcfce7;
  border-color: #4ade80;
}
preview
▸ CSS Positioning
the positioning cases that are hard to keep straight, each with something to actually watch: an absolute child anchored to its relative parent's bottom-right, a fixed element pinned to the viewport while the page scrolls past it, and a sticky header inside its own scroll container so you can watch it catch and hold. every block comments what it's anchored to and why.
html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Positioning Playground</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main>
    <h1>CSS Positioning Playground</h1>

    <section>
      <!-- This child is anchored to the relatively positioned parent because absolute positioning uses the nearest positioned ancestor. -->
      <p class="note">Relative parent with an absolute child in its bottom-right corner.</p>
      <div class="relative-parent">
        <strong>Relative parent</strong>
        <p>This parent establishes the containing block for the absolute child.</p>
        <div class="absolute-child">Absolute child</div>
      </div>
    </section>

    <section>
      <!-- This element is anchored to the viewport because fixed positioning ignores normal document flow and stays in place while the page scrolls. -->
      <div class="fixed-corner">Fixed to viewport</div>
    </section>

    <section>
      <!-- This sticky element is anchored to the scroll container because it sticks to top: 0 while its container is being scrolled. -->
      <p class="note">Scrollable container with a sticky header. Scroll inside the box below.</p>
      <div class="scroll-container">
        <div class="sticky-header">Sticky inside this scroll container</div>
        <div class="scroll-content">
          <p>Scroll this panel and watch the yellow header stay pinned to the top of the container.</p>
          <p>Content row 1: Sticky positioning behaves like relative positioning until the element reaches its threshold.</p>
          <p>Content row 2: Here the threshold is top: 0, measured against the scrollable container.</p>
          <p>Content row 3: The header will keep sticking while its parent container remains in view.</p>
          <p>Content row 4: This makes sticky useful for section labels, table headers, and in-page navigation.</p>
          <p>Content row 5: Keep scrolling to see the sticky behavior continue.</p>
          <p>Content row 6: The element remains part of the normal document flow.</p>
          <p>Content row 7: Unlike fixed positioning, it is constrained by its parent container.</p>
          <p>Content row 8: Once the container's scroll area ends, the sticky behavior ends too.</p>
          <p>Content row 9: A tall container gives enough room to see the effect clearly.</p>
          <p>Content row 10: End of the demo content.</p>
        </div>
      </div>
    </section>
  </main>
</body>
</html>
css
styles.css
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 140vh;
  font-family: Arial, sans-serif;
  color: #1f2937;
  background: #f7f7fb;
}

main {
  width: min(920px, calc(100% - 32px));
  margin: 40px auto 120px;
}

h1 {
  margin: 0 0 24px;
  font-size: 2rem;
}

section {
  margin-bottom: 40px;
}

.note {
  margin: 0 0 10px;
  font-size: 0.95rem;
  color: #4b5563;
}

/* This parent stays in normal flow and anchors the absolute child to itself. */
.relative-parent {
  position: relative;
  min-height: 260px;
  padding: 24px;
  border: 2px dashed #64748b;
  border-radius: 8px;
  background: #ffffff;
}

/* This child is anchored to its relative parent, so bottom-right stays inside it. */
.absolute-child {
  position: absolute;
  right: 16px;
  bottom: 16px;
  padding: 16px 18px;
  border-radius: 8px;
  color: #ffffff;
  background: #2563eb;
  font-weight: 700;
  box-shadow: 0 12px 24px rgba(37, 99, 235, 0.24);
}

/* This element is anchored to the viewport, so it remains bottom-left while the page scrolls. */
.fixed-corner {
  position: fixed;
  left: 18px;
  bottom: 18px;
  z-index: 10;
  padding: 14px 16px;
  border-radius: 8px;
  color: #ffffff;
  background: #047857;
  font-weight: 700;
  box-shadow: 0 12px 24px rgba(4, 120, 87, 0.25);
}

/* This container owns the scrolling area, giving the sticky item a boundary to stick within. */
.scroll-container {
  height: 340px;
  overflow-y: auto;
  border: 2px solid #cbd5e1;
  border-radius: 8px;
  background: #ffffff;
}

/* This header is anchored to the scroll container's top, so it sticks only while that container scrolls. */
.sticky-header {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 16px 20px;
  color: #111827;
  background: #facc15;
  font-weight: 700;
  border-bottom: 1px solid #eab308;
}

.scroll-content {
  padding: 18px 20px 28px;
}

.scroll-content p {
  margin: 0 0 18px;
  line-height: 1.6;
}
preview
▸ CSS Layout
a full small-page skeleton — a sticky header that holds while the page scrolls under it, a flex nav row, two cards splitting a flex-wrap row, a grid nested inside one of them, and a note pulled from the flow entirely with display: none. the layout primitives you actually reach for, in one page instead of five demos.
html
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Layout Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <header class="site-header">
      <h1>Layout Playground</h1>
      <nav class="site-nav" aria-label="Primary navigation">
        <a href="#home">Home</a>
        <a href="#cards">Cards</a>
        <a href="#footer">Contact</a>
      </nav>
    </header>

    <main class="page-main" id="home">
      <h2 id="cards">Two-Card Flexbox Layout</h2>
      <section class="cards" aria-label="Layout examples">
        <article class="card">
          <h3>Flexbox Card</h3>
          <p>This card shares its row with the card beside it.</p>
          <p class="hidden-note">This sentence is hidden with display: none.</p>
        </article>

        <article class="card">
          <h3>Grid Card</h3>
          <p>These small items use a two-column grid.</p>
          <div class="grid-example">
            <div class="grid-item">One</div>
            <div class="grid-item">Two</div>
            <div class="grid-item">Three</div>
            <div class="grid-item">Four</div>
          </div>
        </article>
      </section>

      <section class="content-block" aria-label="Sticky positioning demonstration">
        <h2>Keep scrolling</h2>
        <p>You're scrolling past this content right now, and the header above is staying put — that's <code>position: sticky</code> at work. It behaves like a normal element until it hits its <code>top: 0</code> threshold, then it locks in place and holds while the rest of the page moves underneath it.</p>
        <p>This is different from <code>position: fixed</code>, which anchors to the viewport from the moment the page loads and never moves at all. Sticky only kicks in once you scroll far enough for it to matter — the rest of the time it just sits in normal document flow like any other element.</p>
        <p>It's also different from <code>position: static</code>, the default every element starts with. Static elements just flow top to bottom with everything else and never anchor to anything.</p>
        <p>Sticky positioning is scoped to its nearest scrolling ancestor — in this case, the whole page — so the header sticks relative to the browser window, not to some smaller inner container.</p>
        <p>Once you reach the footer below, the header will simply run out of page to stick to and scroll away with everything else, since there's nothing left underneath it to hold its position against.</p>
      </section>
    </main>

    <footer class="site-footer" id="footer">
      <p>Layout Playground footer</p>
    </footer>
  </body>
</html>
css
styles.css
/* Sets shared colors so the page stays consistent and easy to update. */
:root {
  color: #1f2937;
  background: #f3f4f6;
  font-family: Arial, sans-serif;
}

/* Removes the default page margin so each layout section aligns cleanly. */
body {
  margin: 0;
}

/* Keeps the header visible while scrolling, so navigation is always available. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 1rem 2rem;
  background: #1d4ed8;
  color: white;
}

/* Makes the navigation links sit in a flexible horizontal row with even spacing. */
.site-nav {
  display: flex;
  gap: 1rem;
}

/* Makes each navigation link clear and easy to read against the header. */
.site-nav a {
  color: inherit;
  font-weight: bold;
  text-decoration: none;
}

/* Centers the page content and gives it comfortable breathing room. */
.page-main {
  max-width: 900px;
  margin: 0 auto;
  padding: 3rem 2rem;
}

/* Uses flexbox to place the two cards side by side and wrap them on narrow screens. */
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
}

/* Lets each card grow evenly, keeping the two-card layout balanced. */
.card {
  flex: 1 1 280px;
  padding: 1.5rem;
  border-radius: 0.75rem;
  background: white;
  box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
}

/* Creates a two-column grid so small items line up in both rows and columns. */
.grid-example {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.75rem;
}

/* Styles each grid item so the grid structure is easy to see. */
.grid-item {
  padding: 0.75rem;
  border-radius: 0.5rem;
  background: #dbeafe;
  text-align: center;
}

/* Hides optional content completely so it takes up no page space. */
.hidden-note {
  display: none;
}

/* Gives content sections enough vertical rhythm to actually force scroll,
   so the sticky header below has real distance to demonstrate sticking. */
.content-block {
  margin-top: 2.5rem;
}

.content-block h2 {
  margin-bottom: 0.5rem;
}

.content-block p {
  line-height: 1.6;
  margin: 0 0 1rem;
}

/* Gives the footer a distinct ending area that matches the header. */
.site-footer {
  padding: 1.5rem 2rem;
  background: #1f2937;
  color: white;
  text-align: center;
}
preview
▸ CSS Filters & Transforms
the same image three times with blur(4px), grayscale(100%) and sepia(100%) side by side, so the filters are a direct comparison rather than three separate claims. plus rotate() and scale() stacked on one element, and a box shoved off-screen with translateX(-200vw) — invisible, but still in the accessibility tree and still announced. that last one is the point.
html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Filters and Transforms Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <h1>Filters and Transforms Playground</h1>

    <main>
      <section aria-labelledby="filters-heading">
        <h2 id="filters-heading">CSS Filters</h2>

        <div class="image-row">
          <figure>
            <!-- filter: blur(4px) -->
            <img class="blur" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 440 320'%3E%3Crect width='440' height='320' fill='%2387ceeb'/%3E%3Ccircle cx='350' cy='70' r='42' fill='%23ffd166'/%3E%3Cpath d='M0 230 125 105l95 105 55-65 165 175H0Z' fill='%235d8a66'/%3E%3Cpath d='M0 270 125 145l95 105 55-65 130 135H0Z' fill='%233f6b4b'/%3E%3C/svg%3E" alt="Stylized landscape with mountains and a sun">
            <figcaption>Blur</figcaption>
          </figure>

          <figure>
            <!-- filter: grayscale(100%) -->
            <img class="grayscale" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 440 320'%3E%3Crect width='440' height='320' fill='%2387ceeb'/%3E%3Ccircle cx='350' cy='70' r='42' fill='%23ffd166'/%3E%3Cpath d='M0 230 125 105l95 105 55-65 165 175H0Z' fill='%235d8a66'/%3E%3Cpath d='M0 270 125 145l95 105 55-65 130 135H0Z' fill='%233f6b4b'/%3E%3C/svg%3E" alt="Stylized landscape with mountains and a sun">
            <figcaption>Grayscale</figcaption>
          </figure>

          <figure>
            <!-- filter: sepia(100%) -->
            <img class="sepia" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 440 320'%3E%3Crect width='440' height='320' fill='%2387ceeb'/%3E%3Ccircle cx='350' cy='70' r='42' fill='%23ffd166'/%3E%3Cpath d='M0 230 125 105l95 105 55-65 165 175H0Z' fill='%235d8a66'/%3E%3Cpath d='M0 270 125 145l95 105 55-65 130 135H0Z' fill='%233f6b4b'/%3E%3C/svg%3E" alt="Stylized landscape with mountains and a sun">
            <figcaption>Sepia</figcaption>
          </figure>
        </div>
      </section>

      <section aria-labelledby="transforms-heading">
        <h2 id="transforms-heading">CSS Transforms</h2>

        <div class="box-row">
          <!-- transform: rotate(12deg) scale(1.15) -->
          <div class="box rotate-and-scale">Rotated and scaled</div>

          <!-- transform: translateX(-200vw); this box is invisible off-screen but is STILL in the accessibility tree. -->
          <div class="box off-screen">Moved off-screen</div>
        </div>
      </section>
    </main>
  </body>
</html>
css
styles.css
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 2rem;
  font-family: Arial, sans-serif;
  background-color: #f4f4f4;
  color: #222;
}

h1,
h2 {
  text-align: center;
}

.image-row,
.box-row {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 2rem;
  margin: 2rem auto 4rem;
}

figure {
  margin: 0;
  text-align: center;
}

img {
  display: block;
  width: 220px;
  height: 160px;
  object-fit: cover;
  border-radius: 0.75rem;
}

figcaption {
  margin-top: 0.75rem;
  font-weight: bold;
}

.blur {
  filter: blur(4px);
}

.grayscale {
  filter: grayscale(100%);
}

.sepia {
  filter: sepia(100%);
}

.box {
  display: grid;
  place-items: center;
  width: 180px;
  min-height: 120px;
  padding: 1rem;
  border: 3px solid #583d72;
  border-radius: 0.75rem;
  background-color: #c9b1ff;
  font-weight: bold;
  text-align: center;
}

.rotate-and-scale {
  transform: rotate(12deg) scale(1.15);
}

.off-screen {
  transform: translateX(-200vw);
}

@media (max-width: 800px) {
  .image-row {
    flex-direction: column;
  }
}
preview
▸ CSS Animations
a box that travels and shifts color on a loop via @keyframes and the full animation shorthand — name, duration, timing function, infinite iteration. paired with the standard prefers-reduced-motion: reduce block that collapses animation duration to near-zero, because motion you can't turn off is an accessibility failure, not a flourish.
html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Animations Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <main>
      <h1>Animations Playground</h1>
      <p>The box moves from left to right while changing color.</p>

      <div class="animation-track">
        <!-- animation: move-and-change 3s ease-in-out infinite -->
        <div class="animated-box">Animate!</div>
      </div>
    </main>
  </body>
</html>
css
styles.css
* {
  box-sizing: border-box;
}

body {
  display: grid;
  min-height: 100vh;
  margin: 0;
  padding: 2rem;
  place-items: center;
  font-family: Arial, sans-serif;
  background-color: #f4f4f4;
  color: #222;
}

main {
  width: min(700px, 100%);
}

h1,
p {
  text-align: center;
}

.animation-track {
  width: 100%;
  margin-top: 2rem;
  padding: 1rem;
  overflow: hidden;
  border: 3px solid #333;
  border-radius: 0.75rem;
  background-color: #fff;
}

.animated-box {
  display: grid;
  width: 100px;
  height: 100px;
  place-items: center;
  border-radius: 0.5rem;
  background-color: #4f46e5;
  color: #fff;
  font-weight: bold;
  animation: move-and-change 3s ease-in-out infinite;
}

@keyframes move-and-change {
  from {
    transform: translateX(0);
    background-color: #4f46e5;
  }

  to {
    transform: translateX(calc(min(700px, 100vw - 4rem) - 138px));
    background-color: #e11d48;
  }
}

/* If the user requests reduced motion, this standard fix makes animations finish almost instantly instead of repeatedly moving, while !important ensures it overrides component animation settings. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}
preview
▸ HTML + CSS Forms
semantic form structure done properly — an explicit label wired for/id, an implicit label wrapping its input, three radios correctly grouped by a shared name inside a fieldset with a legend, and a clear button declaring type="button" so it doesn't silently submit. :focus-visible rings throughout.

* this build contains a small amount of working javascript to make the clear button function. the javascript isn't the point and wasn't part of my skill level when this was built — it's here so the html concept can be demonstrated end to end. shouts out to Bonnie for the assist.
html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Forms Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <main>
      <h1>Forms Playground</h1>

      <form id="playground-form" action="#" method="post">
        <div class="form-control">
          <label for="full-name">Full name</label>
          <input type="text" id="full-name" name="full-name">
        </div>

        <label class="form-control">
          Email address
          <input type="email" name="email">
        </label>

        <fieldset>
          <legend>Choose a contact method</legend>

          <label>
            <input type="radio" name="contact-method" value="email">
            Email
          </label>

          <label>
            <input type="radio" name="contact-method" value="phone">
            Phone
          </label>

          <label>
            <input type="radio" name="contact-method" value="text-message">
            Text message
          </label>
        </fieldset>

        <div class="button-row">
          <button type="submit">Submit</button>

          <!-- If type="button" were removed, this button would default to type="submit" and submit the form when clicked. -->
          <button type="button" id="clear-button">Clear</button>
        </div>
      </form>
    </main>

    <script>
      const form = document.querySelector("#playground-form");
      const clearButton = document.querySelector("#clear-button");

      clearButton.addEventListener("click", () => {
        form.reset();
      });
    </script>
  </body>
</html>
css
styles.css
* {
  box-sizing: border-box;
}

body {
  display: grid;
  min-height: 100vh;
  margin: 0;
  padding: 2rem;
  place-items: center;
  font-family: Arial, sans-serif;
  background-color: #f3f4f6;
  color: #1f2937;
}

main {
  width: min(500px, 100%);
  padding: 2rem;
  border-radius: 1rem;
  background-color: #fff;
  box-shadow: 0 10px 30px rgb(0 0 0 / 10%);
}

h1 {
  margin-top: 0;
  text-align: center;
}

form {
  display: grid;
  gap: 1.5rem;
}

.form-control {
  display: grid;
  gap: 0.5rem;
}

label,
legend {
  font-weight: bold;
}

input[type="text"],
input[type="email"] {
  width: 100%;
  padding: 0.75rem;
  border: 2px solid #9ca3af;
  border-radius: 0.5rem;
  font: inherit;
}

fieldset {
  display: grid;
  gap: 0.75rem;
  margin: 0;
  padding: 1rem;
  border: 2px solid #9ca3af;
  border-radius: 0.5rem;
}

fieldset label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: normal;
}

.button-row {
  display: flex;
  gap: 1rem;
}

button {
  flex: 1;
  padding: 0.75rem 1rem;
  border: 0;
  border-radius: 0.5rem;
  font: inherit;
  font-weight: bold;
  cursor: pointer;
}

button[type="submit"] {
  background-color: #2563eb;
  color: #fff;
}

button[type="button"] {
  background-color: #e5e7eb;
  color: #1f2937;
}

input:focus-visible,
button:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
preview
▸ HTML + CSS Accessibility
four techniques that are invisible by design — that's what makes them accessibility work. an icon-only button given a name through aria-label, a decorative image pulled out of the accessibility tree with aria-hidden, text hidden visually but not from screen readers via the full .sr-only rule, and an input stripped with appearance: none then given its :focus ring back by hand. nothing here looks like much. that's the whole idea — every comment names what a screen reader gets that your eyes don't.
html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessibility Playground</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <main>
      <h1>Accessibility Playground</h1>

      <section>
        <h2>Icon-only button</h2>

        <!-- This icon-only button uses aria-label to provide the programmatic name "Close notification" to assistive technology. -->
        <button class="icon-button" type="button" aria-label="Close notification">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
            <path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
          </svg>
        </button>
      </section>

      <section>
        <h2>Decorative image</h2>

        <!-- This image is purely decorative, so aria-hidden="true" keeps it out of the accessibility tree. -->
        <img class="decorative-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 120'%3E%3Cpath d='M10 75C70 5 125 115 185 45s95 20 125-15' fill='none' stroke='%237c3aed' stroke-width='18' stroke-linecap='round'/%3E%3Ccircle cx='60' cy='35' r='16' fill='%23f59e0b'/%3E%3Ccircle cx='260' cy='85' r='20' fill='%2306b6d4'/%3E%3C/svg%3E" alt="" aria-hidden="true">
      </section>

      <section>
        <h2>Screen-reader-only text</h2>

        <!-- This text is visually hidden by .sr-only but remains available to screen readers. -->
        <p class="sr-only">This sentence is available to screen readers even though it is not visible on the page.</p>

        <p>The screen-reader-only sentence is hidden just above this visible explanation.</p>
      </section>

      <section>
        <h2>Custom input appearance</h2>

        <div class="input-group">
          <label for="custom-name">Display name</label>

          <!-- This input removes its native appearance, then manually restores a visible border and :focus outline in CSS. -->
          <input class="custom-input" type="text" id="custom-name" name="custom-name">
        </div>
      </section>
    </main>
  </body>
</html>
css
styles.css
* {
  box-sizing: border-box;
}

body {
  display: grid;
  min-height: 100vh;
  margin: 0;
  padding: 2rem;
  place-items: center;
  font-family: Arial, sans-serif;
  background-color: #f3f4f6;
  color: #1f2937;
}

main {
  display: grid;
  width: min(600px, 100%);
  gap: 2rem;
  padding: 2rem;
  border-radius: 1rem;
  background-color: #fff;
  box-shadow: 0 10px 30px rgb(0 0 0 / 10%);
}

h1 {
  margin: 0;
  text-align: center;
}

section {
  padding: 1.25rem;
  border: 2px solid #d1d5db;
  border-radius: 0.75rem;
}

h2 {
  margin-top: 0;
}

.icon-button {
  display: grid;
  width: 48px;
  height: 48px;
  padding: 0;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background-color: #dc2626;
  color: #fff;
  cursor: pointer;
}

.icon-button svg {
  width: 24px;
  height: 24px;
}

.decorative-image {
  display: block;
  width: 160px;
  height: auto;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.input-group {
  display: grid;
  gap: 0.5rem;
}

.input-group label {
  font-weight: bold;
}

.custom-input {
  width: 100%;
  padding: 0.75rem;
  appearance: none;
  border: 2px solid #6b7280;
  border-radius: 0.5rem;
  background-color: #fff;
  color: #111827;
  font: inherit;
}

.custom-input:focus {
  border-color: #1d4ed8;
  outline: 3px solid #93c5fd;
  outline-offset: 3px;
}
preview
▸ The Odin Project 2 built · worked chronologically
▸ HTML "Build a Recipe" Project
the Odin Project's take on the fCC recipe lab — a pure semantic HTML page: an h1 title, a hero img with alt text, a description, an ul ingredients list, an ol of ordered steps, and a "more recipes" link list. no CSS, structure only. every line commented.
html
index.html
<!DOCTYPE html>                          <!-- tells the browser this is modern HTML5 -->
<html lang="en">                         <!-- starts the page; lang helps screen readers know this is English -->
<head>                                   <!-- holds page info the browser uses; this part does not show on the page -->
<title>The Odin Recipes</title>           <!-- sets the browser tab title; the test needs this exact text -->
</head>
<body>
<h1>Creamy Chocolate Fudge</h1>           <!-- main visible heading for the recipe; the test needs this exact text -->
<img src="https://cdn.freecodecamp.org/curriculum/labs/recipe.jpg" alt="creamy chocolate fudge squares on a plate"> <!-- shows the recipe image; alt describes it for screen readers and if the image fails -->
<h2>Description</h2> <!--starts the description section; the Odin Project FCC test needs this exact heading-->
<p>This creamy chocolate fudge is rich, smooth, and simple enough for a beginner recipe page.</p> <!-- first description paragraph; the test needs at least two p elements -->
<p>It uses basic pantry ingredients and sets into sweet squares that are easy to share.</p> <!-- second description paragraph; this gives the section more recipe detail -->
<h2>Ingredients</h2>                      <!-- starts the ingredients section; the test needs this exact heading text -->
<ul>                                      <!-- starts an unordered list because ingredients do not need step-by-step order -->
  <li>2 cups chocolate chips</li>         <!-- one ingredient item in the unordered list -->
  <li>1 can sweetened condensed milk</li> <!-- one ingredient item in the unordered list -->
  <li>2 tablespoons butter</li>           <!-- one ingredient item in the unordered list -->
  <li>1 teaspoon vanilla extract</li>     <!-- one ingredient item in the unordered list -->
</ul>                                     <!-- closes the ingredients list -->
<h2>Steps</h2>                            <!-- starts the recipe steps section; the test needs this exact heading text -->
<ol>                                      <!-- starts an ordered list because recipe steps must be followed in sequence -->
  <li>Line a small pan with parchment paper.</li> <!-- first numbered step in the recipe -->
  <li>Melt the chocolate chips, condensed milk, and butter together.</li> <!-- second numbered step in the recipe -->
  <li>Stir in the vanilla extract.</li>   <!-- third numbered step in the recipe -->
  <li>Spread the mixture into the pan and let it set before cutting.</li> <!-- fourth numbered step in the recipe -->
</ol>                                     <!-- closes the ordered steps list -->
<h2>More Recipes</h2>                     <!-- starts the related recipes section; the test needs this exact heading text -->
<ul>                                      <!-- starts an unordered list because these recipe links are related options, not steps -->
  <li><a href="#">Vanilla Cupcakes</a></li> <!-- list item with a placeholder link; the test needs href set to # -->
  <li><a href="#">Peanut Butter Cookies</a></li> <!-- list item with a placeholder link; the test needs href set to # -->
</ul>                                     <!-- closes the more recipes list -->
<ul>                                      <!-- starts an unordered list because these recipe links are related options, not steps -->
  <li><a href="#">Vanilla Cupcakes</a></li> <!-- list item with a placeholder link; the test needs href set to # -->
  <li><a href="#">Peanut Butter Cookies</a></li> <!-- list item with a placeholder link; the test needs href set to # -->
</ul>                                     <!-- closes the more recipes list -->
</body>
</html>
preview
▸ HTML + CSS "Landing Page" Project
the capstone of TOP's Foundations course — a full landing page rebuilt from a design mockup: a dark header and hero with a Sign up button, a four-card info row, a testimonial quote, a blue call-to-action box, and a footer. flexbox handles every layout, and the colors from the mockup live in CSS custom properties (--dark-bg, --accent-blue) so they're set once and reused.
html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Landing Page</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <header>
    <div class="header-content">
      <div class="logo">Header Logo</div>
      <ul class="nav-links">
        <li><a href="#">header link one</a></li>
        <li><a href="#">header link two</a></li>
        <li><a href="#">header link three</a></li>
      </ul>
    </div>
  </header>

  <section class="hero">
    <div class="hero-content">
      <div class="hero-text">
        <h1>This website is awesome</h1>
        <p>This website has some subtext that goes here under the main title. It's a smaller font and the color is lower contrast.</p>
        <button>Sign up</button>
      </div>
      <div class="hero-img">this is a placeholder for an image</div>
    </div>
  </section>

  <section class="info">
    <h2>Some random information.</h2>
    <div class="cards">
      <div class="card">
        <div class="card-img"></div>
        <p>this is some subtext under an illustration or image</p>
      </div>
      <div class="card">
        <div class="card-img"></div>
        <p>this is some subtext under an illustration or image</p>
      </div>
      <div class="card">
        <div class="card-img"></div>
        <p>this is some subtext under an illustration or image</p>
      </div>
      <div class="card">
        <div class="card-img"></div>
        <p>this is some subtext under an illustration or image</p>
      </div>
    </div>
  </section>

  <section class="quote">
    <div class="quote-content">
      <p class="quote-text">This is an inspiring quote, or a testimonial from a customer. Maybe it's just filling up space, or maybe people will actually read it. Who knows? All I know is that it looks nice.</p>
      <p class="quote-author">-Thor, God of Thunder</p>
    </div>
  </section>

  <section class="cta">
    <div class="cta-box">
      <div class="cta-text">
        <h3>Call to action! It's time!</h3>
        <p>Sign up for our product by clicking that button right over there!</p>
      </div>
      <button>Sign up</button>
    </div>
  </section>

  <footer>
    <p>Copyright © The Odin Project 2021</p>
  </footer>
</body>
</html>
css
styles.css
/* ---- dedsign tokens from Image Two ---- */
:root {
    --dark-bg: #1F2937;
    --hero-main-text:#F9FAF8;
    --hero-secondary-text: #E5E7EB;
    --accent-blue: #3882f6;
    --quote-bg: #E5e7eb;
    --info-header-text:#1F2937;
}

/* ---- reset + base ---- */
* { margin: 0; padding: 0; box-sizing: border-box; }

body { font-family: Roboto, -apple-system, Helvetica, Arial, sans-serif; }

.header-content, .hero-content, .info,
.quote-content, .cta-box {
  max-width: 1000px;
  margin: 0 auto;
}

/* ---- header ---- */
header { background-color: var(--dark-bg); padding: 16px 0; }
.header-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.logo { font-size: 24px; font-weight: 700; color: var(--hero-main-text); }
.nav-links { display: flex; gap: 24px; list-style: none; }
.nav-links a {
  font-size: 18px;
  color: var(--hero-secondary-text);
  text-decoration: none;
}

/* ---- hero ---- */
.hero { background-color: var(--dark-bg); padding: 100px 0 120px; }
.hero-content { display: flex; align-items: center; gap: 48px; }
.hero-text { flex: 1; }
.hero-text h1 {
  font-size: 48px;
  font-weight: 900;
  color: var(--hero-main-text);
  line-height: 1.1;
}
.hero-text p {
  font-size: 18px;
  color: var(--hero-secondary-text);
  margin: 8px 0 12px;
}
.hero-img {
  flex: 1;
  background-color: #6d747f;
  color: var(--hero-secondary-text);
  height: 230px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
}
button {
  background-color: var(--accent-blue);
  color: var(--hero-main-text);
  font-size: 16px;
  font-weight: 700;
  border: none;
  border-radius: 8px;
  padding: 8px 32px;
  cursor: pointer;
}

/* ---- info cards ---- */
.info { padding: 48px 0 100px; }
.info h2 {
  font-size: 36px;
  font-weight: 900;
  color: var(--info-header-text);
  text-align: center;
  margin-bottom: 48px;
}
.cards { display: flex; justify-content: center; gap: 56px; }
.card { width: 160px; text-align: center; }
.card-img {
  width: 160px;
  height: 160px;
  border: 4px solid var(--accent-blue);
  border-radius: 12px;
  margin-bottom: 8px;
}
.card p { color: #4b5563; }

/* ---- quote ---- */
.quote { background-color: var(--quote-bg); padding: 120px 0; }
.quote-text {
  font-size: 36px;
  font-weight: 300;
  font-style: italic;
  color: var(--info-header-text);
  max-width: 800px;
  margin: 0 auto;
}
.quote-author {
  font-size: 22px;
  font-weight: 700;
  color: var(--info-header-text);
  text-align: right;
  max-width: 800px;
  margin: 12px auto 0;
}

/* ---- call to action ---- */
.cta { padding: 100px 0; }
.cta-box {
  background-color: var(--accent-blue);
  border-radius: 8px;
  padding: 48px 120px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 48px;
}
.cta-text h3 { font-size: 22px; color: var(--hero-main-text); }
.cta-text p { color: var(--hero-secondary-text); }
.cta button { border: 2px solid var(--hero-main-text); white-space: nowrap; }

/* ---- footer ---- */
footer {
  background-color: var(--dark-bg);
  color: var(--hero-secondary-text);
  text-align: center;
  padding: 40px 0;
}
preview