DIY — Project Detail
Instructables-style maker project page for a fictional Mason Jar Herb Garden with Auto-Watering: a blueprint-grid hero with author chip, views and favorites, difficulty and time badges, a materials and tools checklist with mono part numbers and live progress, five collapsible build steps with smooth JS expand, diagram figures and tip or caution callouts, a community I Made It gallery, a comment thread with likes and replies, and a sticky action bar with a localStorage-persisted favorite heart, share, and PDF toasts.
MCP
Code
:root {
--blueprint: #0f2a4a;
--blueprint-d: #0a1e36;
--blueprint-l: #16385f;
--grid: rgba(140, 180, 220, 0.14);
--grid-major: rgba(140, 180, 220, 0.26);
--orange: #ff6b35;
--orange-d: #e0521f;
--orange-soft: #ffe3d6;
--kraft: #d9c7a7;
--kraft-l: #efe6d4;
--ink: #1c2733;
--ink-2: #3d4a57;
--muted: #6b7885;
--bg: #f4f1ea;
--white: #ffffff;
--line: rgba(28, 39, 51, 0.12);
--line-2: rgba(28, 39, 51, 0.2);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--shadow: 0 1px 2px rgba(28, 39, 51, 0.05), 0 8px 24px rgba(28, 39, 51, 0.07);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
padding-bottom: 96px;
}
.mono { font-family: "JetBrains Mono", ui-monospace, monospace; }
.sr-only {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap;
}
/* ============ HERO ============ */
.hero {
position: relative;
background: var(--blueprint);
background-image:
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px),
linear-gradient(var(--grid-major) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-major) 1px, transparent 1px);
background-size: 8px 8px, 8px 8px, 80px 80px, 80px 80px;
color: var(--kraft-l);
padding: 40px 24px 56px;
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
gap: 40px;
align-items: center;
}
.hero-inner { max-width: 640px; margin-left: auto; width: 100%; }
.crumbs {
display: flex; flex-wrap: wrap; gap: 8px;
font-size: 13px; color: rgba(239, 230, 212, 0.7);
margin-bottom: 22px;
}
.crumbs a { color: inherit; text-decoration: none; }
.crumbs a:hover, .crumbs a:focus-visible { color: var(--orange); }
.crumbs [aria-current] { color: var(--kraft-l); }
.project-no {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 12px; letter-spacing: 0.18em;
color: rgba(239, 230, 212, 0.6);
margin-bottom: 10px;
}
.project-no span { color: var(--orange); font-weight: 700; }
.hero-title {
font-size: clamp(30px, 4.4vw, 48px);
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.03em;
line-height: 1.1;
color: var(--white);
}
.hero-title em { font-style: normal; color: var(--orange); }
.hero-badges { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0 26px; }
.badge {
display: inline-flex; align-items: center; gap: 7px;
padding: 6px 12px;
border-radius: 999px;
font-size: 13px; font-weight: 600;
border: 1px solid rgba(239, 230, 212, 0.3);
color: var(--kraft-l);
background: rgba(255, 255, 255, 0.05);
}
.badge-difficulty { border-color: var(--orange); color: var(--orange); background: rgba(255, 107, 53, 0.1); }
.badge .mono { font-size: 12px; font-weight: 700; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 20px 28px; align-items: center; }
.author-chip {
display: flex; align-items: center; gap: 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(239, 230, 212, 0.18);
border-radius: 999px;
padding: 8px 10px 8px 8px;
}
.avatar {
width: 38px; height: 38px; flex: none;
border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 13px;
color: var(--white);
background: var(--ink-2);
}
.avatar-hero { background: linear-gradient(135deg, var(--orange), var(--orange-d)); }
.av-g { background: var(--ok); }
.av-b { background: var(--blueprint-l); }
.av-o { background: var(--warn); }
.av-k { background: #8a744f; }
.author-txt { display: flex; flex-direction: column; line-height: 1.3; }
.author-name { color: var(--white); font-weight: 700; font-size: 14px; text-decoration: none; }
.author-name:hover, .author-name:focus-visible { color: var(--orange); }
.author-sub { font-size: 12px; color: rgba(239, 230, 212, 0.65); }
.btn-follow {
font: inherit; font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--blueprint-d);
background: var(--kraft);
border: none; border-radius: 999px;
padding: 7px 14px;
cursor: pointer;
transition: background 0.15s, transform 0.15s;
}
.btn-follow:hover { background: var(--kraft-l); }
.btn-follow:active { transform: scale(0.96); }
.btn-follow.is-following { background: var(--ok); color: var(--white); }
.hero-stats { display: flex; gap: 18px; font-size: 13px; color: rgba(239, 230, 212, 0.75); }
.stat strong { color: var(--white); font-size: 14px; }
/* hero figure */
.hero-figure { max-width: 460px; width: 100%; justify-self: start; }
.fig-frame {
position: relative;
border: 1px dashed rgba(140, 180, 220, 0.5);
border-radius: var(--r-sm);
padding: 26px;
}
.fig-corner {
position: absolute; width: 12px; height: 12px;
border: 2px solid var(--orange);
}
.fig-corner.tl { top: -2px; left: -2px; border-right: 0; border-bottom: 0; }
.fig-corner.tr { top: -2px; right: -2px; border-left: 0; border-bottom: 0; }
.fig-corner.bl { bottom: -2px; left: -2px; border-right: 0; border-top: 0; }
.fig-corner.br { bottom: -2px; right: -2px; border-left: 0; border-top: 0; }
.fig-art {
position: relative; height: 220px;
display: flex; align-items: flex-end; justify-content: center; gap: 26px;
padding-bottom: 34px;
}
.fig-jar {
width: 64px; height: 96px;
border: 2px solid rgba(239, 230, 212, 0.85);
border-radius: 6px 6px 14px 14px;
position: relative;
background: linear-gradient(to top, rgba(217, 199, 167, 0.25) 45%, transparent 45%);
}
.fig-jar::before {
content: ""; position: absolute; top: -10px; left: 8px; right: 8px; height: 8px;
border: 2px solid rgba(239, 230, 212, 0.85);
border-radius: 3px;
}
.fig-jar::after {
content: ""; position: absolute; top: -34px; left: 50%; transform: translateX(-50%);
width: 2px; height: 26px;
background: repeating-linear-gradient(var(--ok) 0 4px, transparent 4px 7px);
}
.fig-jar.j2 { height: 110px; }
.fig-tube {
position: absolute; left: 8%; right: 8%; top: 46px; height: 2px;
background: repeating-linear-gradient(90deg, var(--orange) 0 8px, transparent 8px 13px);
}
.fig-label {
position: absolute; bottom: 0; left: 50%; transform: translateX(-50%);
font-size: 11px; letter-spacing: 0.16em;
color: rgba(140, 180, 220, 0.9);
white-space: nowrap;
}
/* ============ PAGE / CARDS ============ */
.page {
max-width: 880px;
margin: -28px auto 0;
padding: 0 20px;
position: relative;
display: grid;
gap: 28px;
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 28px 30px;
}
.section-h {
font-size: 15px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--ink);
display: flex; align-items: center; gap: 10px;
margin-bottom: 14px;
}
.section-h::before {
content: ""; width: 22px; height: 3px;
background: var(--orange); border-radius: 2px;
}
.intro p + p { margin-top: 12px; }
.intro p { color: var(--ink-2); max-width: 68ch; }
/* ============ MATERIALS ============ */
.materials-head {
display: flex; align-items: baseline; justify-content: space-between;
gap: 12px; flex-wrap: wrap;
}
.mat-progress {
font-size: 12px; font-weight: 700;
color: var(--muted);
background: var(--kraft-l);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px 12px;
}
.mat-progress.is-done { color: var(--white); background: var(--ok); border-color: var(--ok); }
.mat-cols {
display: grid; grid-template-columns: 1.3fr 1fr; gap: 8px 32px;
margin-top: 6px;
}
.mat-group-h {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
color: var(--muted);
margin: 10px 0 6px;
}
.checklist { list-style: none; }
.check-row {
display: flex; align-items: center; gap: 10px;
padding: 9px 10px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.12s;
}
.check-row:hover { background: var(--kraft-l); }
.check-row input { position: absolute; opacity: 0; pointer-events: none; }
.check-row .box {
flex: none; width: 18px; height: 18px;
border: 2px solid var(--line-2);
border-radius: 4px;
position: relative;
transition: background 0.15s, border-color 0.15s;
}
.check-row input:checked + .box { background: var(--ok); border-color: var(--ok); }
.check-row input:checked + .box::after {
content: ""; position: absolute; left: 4px; top: 1px;
width: 5px; height: 9px;
border: solid var(--white); border-width: 0 2px 2px 0;
transform: rotate(42deg);
}
.check-row input:focus-visible + .box { outline: 2px solid var(--orange); outline-offset: 2px; }
.check-txt { flex: 1; font-size: 14px; font-weight: 500; }
.check-row input:checked ~ .check-txt { color: var(--muted); text-decoration: line-through; }
.qty { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.part {
font-size: 11px; font-weight: 700;
color: var(--blueprint);
background: rgba(15, 42, 74, 0.07);
border: 1px solid rgba(15, 42, 74, 0.15);
border-radius: 4px;
padding: 2px 7px;
letter-spacing: 0.04em;
}
.mat-note {
margin-top: 16px;
display: flex; align-items: center; gap: 9px;
font-size: 13px; font-weight: 600;
color: var(--warn);
border-top: 1px dashed var(--line-2);
padding-top: 14px;
}
/* ============ STEPS ============ */
.steps { display: grid; gap: 14px; }
.steps-title { margin-bottom: 0; }
.step { padding: 0; overflow: hidden; }
.step summary {
list-style: none;
display: flex; align-items: center; gap: 14px;
padding: 18px 24px;
cursor: pointer;
user-select: none;
transition: background 0.12s;
}
.step summary::-webkit-details-marker { display: none; }
.step summary:hover { background: var(--kraft-l); }
.step summary:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
.step-no {
font-size: 11px; font-weight: 700; letter-spacing: 0.16em;
color: var(--white);
background: var(--blueprint);
border-radius: 4px;
padding: 4px 9px;
flex: none;
}
.step[open] .step-no { background: var(--orange); }
.step-name { flex: 1; font-weight: 700; font-size: 16px; }
.step-time { font-size: 12px; color: var(--muted); flex: none; }
.chev {
flex: none; width: 9px; height: 9px;
border-right: 2px solid var(--muted);
border-bottom: 2px solid var(--muted);
transform: rotate(45deg);
transition: transform 0.25s;
margin-top: -4px;
}
.step[open] .chev { transform: rotate(225deg); margin-top: 4px; }
.step-body { overflow: hidden; }
.step-inner { padding: 4px 24px 26px; }
.step-inner > p { color: var(--ink-2); max-width: 70ch; margin-top: 14px; }
.step-fig {
position: relative;
background: var(--blueprint);
background-image:
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 8px 8px;
border-radius: var(--r-md);
min-height: 150px;
display: flex; align-items: center; justify-content: center;
padding: 34px 20px 20px;
}
.fig-tag {
position: absolute; top: 10px; left: 14px;
font-size: 10px; letter-spacing: 0.14em;
color: rgba(140, 180, 220, 0.95);
}
.fig-holes { display: flex; gap: 30px; }
.fig-holes i {
width: 56px; height: 56px; border-radius: 50%;
border: 2px dashed var(--orange);
}
.fig-lid { position: relative; width: 110px; height: 110px; border-radius: 50%; border: 2px solid rgba(239,230,212,0.85); }
.fig-lid .hole {
position: absolute; top: 24px; left: 30px;
width: 14px; height: 14px; border-radius: 50%;
border: 2px dashed var(--orange);
}
.fig-lid .slot {
position: absolute; bottom: 24px; right: 26px;
width: 30px; height: 12px; border-radius: 6px;
border: 2px dashed var(--orange);
}
.fig-wires { display: flex; flex-direction: column; gap: 14px; width: 70%; max-width: 300px; }
.fig-wires i { height: 2px; background: repeating-linear-gradient(90deg, var(--orange) 0 10px, transparent 10px 16px); position: relative; }
.fig-wires i::before, .fig-wires i::after {
content: ""; position: absolute; top: -4px; width: 10px; height: 10px;
border-radius: 50%; border: 2px solid rgba(239, 230, 212, 0.85);
}
.fig-wires i::before { left: -14px; }
.fig-wires i::after { right: -14px; border-color: var(--orange); }
.fig-final { position: relative; display: flex; gap: 26px; align-items: flex-end; padding-top: 14px; }
.fig-final i {
width: 40px; height: 58px;
border: 2px solid rgba(239, 230, 212, 0.85);
border-radius: 4px 4px 9px 9px;
}
.fig-final b {
position: absolute; top: 0; left: 10%; right: 10%; height: 2px;
background: repeating-linear-gradient(90deg, var(--orange) 0 8px, transparent 8px 13px);
}
.code-fig { display: block; padding: 40px 20px 20px; }
.code-fig pre {
overflow-x: auto;
font-size: 12.5px; line-height: 1.7;
color: var(--kraft-l);
}
.code-fig code { font-family: inherit; }
.pin {
font-size: 12px; font-weight: 700;
background: rgba(15, 42, 74, 0.08);
border: 1px solid rgba(15, 42, 74, 0.18);
border-radius: 4px;
padding: 1px 6px;
color: var(--blueprint);
}
.callout {
display: flex; gap: 12px; align-items: flex-start;
border-radius: var(--r-md);
padding: 13px 16px;
margin-top: 16px;
font-size: 14px;
}
.callout p { color: inherit; }
.callout-tag {
flex: none;
font-size: 10px; font-weight: 700; letter-spacing: 0.16em;
border-radius: 4px;
padding: 3px 8px;
margin-top: 1px;
}
.callout.tip { background: rgba(47, 158, 111, 0.09); color: #1e6b4a; border: 1px solid rgba(47, 158, 111, 0.25); }
.callout.tip .callout-tag { background: var(--ok); color: var(--white); }
.callout.warn { background: rgba(217, 138, 43, 0.1); color: #8a5514; border: 1px solid rgba(217, 138, 43, 0.3); }
.callout.warn .callout-tag { background: var(--warn); color: var(--white); }
/* ============ I MADE IT ============ */
.made-head {
display: flex; align-items: center; justify-content: space-between;
gap: 14px; flex-wrap: wrap;
}
.made-head .section-h { margin-bottom: 0; }
.btn {
font: inherit; font-size: 13px; font-weight: 700;
border-radius: 999px;
padding: 9px 18px;
cursor: pointer;
transition: background 0.15s, color 0.15s, transform 0.15s, border-color 0.15s;
}
.btn:active { transform: scale(0.97); }
.btn-outline {
background: transparent;
color: var(--orange-d);
border: 2px dashed var(--orange);
}
.btn-outline:hover { background: var(--orange-soft); }
.btn-solid {
background: var(--orange);
color: var(--white);
border: 2px solid var(--orange);
}
.btn-solid:hover { background: var(--orange-d); border-color: var(--orange-d); }
.made-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px;
margin-top: 20px;
}
.make-card {
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
transition: transform 0.18s, box-shadow 0.18s;
}
.make-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
.make-img {
height: 110px;
display: flex; align-items: flex-end; justify-content: flex-end;
padding: 10px 12px;
}
.make-img span { font-size: 10px; letter-spacing: 0.14em; color: rgba(255, 255, 255, 0.85); }
.mk-1 { background: linear-gradient(135deg, var(--blueprint-l), var(--blueprint-d)); }
.mk-2 { background: linear-gradient(135deg, #2f9e6f, #1e6b4a); }
.mk-3 { background: linear-gradient(135deg, var(--orange), var(--orange-d)); }
.mk-4 { background: linear-gradient(135deg, #8a744f, #5f4d31); }
.make-meta { display: flex; gap: 12px; padding: 14px; align-items: flex-start; }
.make-meta .avatar { width: 34px; height: 34px; font-size: 12px; }
.make-name { font-weight: 700; font-size: 14px; display: block; }
.make-cap { font-size: 13px; color: var(--muted); margin-top: 3px; }
/* ============ COMMENTS ============ */
.cmt-count { font-size: 13px; color: var(--muted); font-weight: 600; }
.cmt-list { list-style: none; display: grid; gap: 6px; margin-top: 6px; }
.cmt {
display: flex; gap: 14px;
padding: 16px 12px;
border-radius: var(--r-md);
}
.cmt + .cmt { border-top: 1px dashed var(--line); }
.cmt-author { background: var(--kraft-l); border-top: none !important; }
.cmt-body { flex: 1; min-width: 0; }
.cmt-top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.cmt-name { font-weight: 700; font-size: 14px; }
.author-tag {
font-size: 9px; letter-spacing: 0.14em;
background: var(--orange); color: var(--white);
border-radius: 3px; padding: 2px 6px;
vertical-align: 2px;
}
.cmt-when { font-size: 11px; color: var(--muted); }
.cmt-body > p { font-size: 14px; color: var(--ink-2); margin-top: 4px; }
.cmt-actions { display: flex; gap: 8px; margin-top: 8px; }
.cmt-btn {
font: inherit; font-size: 12px; font-weight: 700;
color: var(--muted);
background: transparent;
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 4px 12px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.cmt-btn:hover { color: var(--orange-d); border-color: var(--orange); background: var(--orange-soft); }
.cmt-like.is-liked { color: var(--white); background: var(--orange); border-color: var(--orange); }
.cmt-form {
display: flex; gap: 10px;
margin-top: 18px;
border-top: 1px solid var(--line);
padding-top: 18px;
}
.cmt-form input {
flex: 1; min-width: 0;
font: inherit; font-size: 14px;
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 10px 18px;
background: var(--bg);
color: var(--ink);
}
.cmt-form input:focus { outline: 2px solid var(--orange); outline-offset: 1px; border-color: var(--orange); }
/* ============ STICKY ACTION BAR ============ */
.action-bar {
position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%);
display: flex; gap: 8px;
background: var(--blueprint-d);
border: 1px solid rgba(140, 180, 220, 0.25);
border-radius: 999px;
padding: 8px;
box-shadow: 0 10px 34px rgba(10, 30, 54, 0.4);
z-index: 40;
}
.act {
display: inline-flex; align-items: center; gap: 8px;
font: inherit; font-size: 13px; font-weight: 700;
color: var(--kraft-l);
background: transparent;
border: none; border-radius: 999px;
padding: 10px 16px;
cursor: pointer;
transition: background 0.15s, color 0.15s, transform 0.15s;
}
.act:hover { background: rgba(255, 255, 255, 0.09); }
.act:active { transform: scale(0.96); }
.act:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.act-cta { background: var(--orange); color: var(--white); }
.act-cta:hover { background: var(--orange-d); }
.act-fav[aria-pressed="true"] { color: var(--orange); }
.act-fav[aria-pressed="true"] .heart { fill: var(--orange); stroke: var(--orange); }
.act-fav.pop svg { animation: heart-pop 0.35s ease; }
@keyframes heart-pop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
100% { transform: scale(1); }
}
/* ============ TOAST ============ */
.toast-region {
position: fixed; left: 50%; bottom: 84px; transform: translateX(-50%);
display: flex; flex-direction: column; align-items: center; gap: 8px;
z-index: 50;
pointer-events: none;
width: min(92vw, 460px);
}
.toast {
background: var(--ink);
color: var(--kraft-l);
font-size: 13px; font-weight: 600;
border-left: 3px solid var(--orange);
border-radius: var(--r-sm);
padding: 11px 18px;
box-shadow: 0 8px 24px rgba(28, 39, 51, 0.35);
opacity: 0;
transform: translateY(8px);
transition: opacity 0.25s, transform 0.25s;
max-width: 100%;
text-align: center;
}
.toast.show { opacity: 1; transform: translateY(0); }
/* ============ RESPONSIVE ============ */
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; padding-bottom: 48px; }
.hero-inner { margin-left: 0; max-width: none; }
.hero-figure { display: none; }
.mat-cols { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.hero { padding: 28px 16px 44px; }
.hero-meta { flex-direction: column; align-items: flex-start; gap: 14px; }
.page { padding: 0 12px; gap: 20px; }
.card { padding: 20px 16px; border-radius: var(--r-md); }
.step summary { padding: 14px 16px; gap: 10px; }
.step-time { display: none; }
.step-inner { padding: 2px 16px 20px; }
.made-grid { grid-template-columns: 1fr; }
.check-row { flex-wrap: wrap; }
.check-txt { flex-basis: calc(100% - 60px); }
.part { margin-left: 28px; }
.action-bar { left: 12px; right: 12px; transform: none; justify-content: center; }
.act { padding: 10px 12px; font-size: 12px; }
.hero-stats { flex-wrap: wrap; gap: 10px 16px; }
}/* DIY — Project Detail interactions (vanilla JS) */
(() => {
"use strict";
/* ---------- toast helper ---------- */
const toastRegion = document.querySelector(".toast-region");
function toast(msg) {
const el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
toastRegion.appendChild(el);
requestAnimationFrame(() => el.classList.add("show"));
setTimeout(() => {
el.classList.remove("show");
el.addEventListener("transitionend", () => el.remove(), { once: true });
setTimeout(() => el.remove(), 500); // fallback
}, 2600);
}
/* generic [data-toast] buttons */
document.querySelectorAll("[data-toast]").forEach((btn) => {
btn.addEventListener("click", () => toast(btn.dataset.toast));
});
/* ---------- materials checklist progress ---------- */
const matBoxes = Array.from(
document.querySelectorAll("[data-checklist] input[type=checkbox]")
);
const matProgress = document.querySelector("[data-mat-progress]");
function updateProgress() {
const done = matBoxes.filter((b) => b.checked).length;
matProgress.textContent = `${done} / ${matBoxes.length} gathered`;
matProgress.classList.toggle("is-done", done === matBoxes.length);
if (done === matBoxes.length) {
toast("Everything gathered — time to make some sawdust!");
}
}
matBoxes.forEach((b) => b.addEventListener("change", updateProgress));
updateProgress();
/* ---------- smooth expand/collapse for <details> steps ---------- */
document.querySelectorAll("details.step").forEach((step) => {
const summary = step.querySelector("summary");
const body = step.querySelector(".step-body");
let animating = false;
summary.addEventListener("click", (e) => {
e.preventDefault();
if (animating) return;
step.hasAttribute("open") ? collapse() : expand();
});
function expand() {
animating = true;
step.setAttribute("open", "");
const h = body.scrollHeight;
body.style.height = "0px";
body.style.opacity = "0";
requestAnimationFrame(() => {
body.style.transition = "height 0.3s ease, opacity 0.3s ease";
body.style.height = h + "px";
body.style.opacity = "1";
});
body.addEventListener("transitionend", function done(ev) {
if (ev.propertyName !== "height") return;
body.style.transition = body.style.height = body.style.opacity = "";
body.removeEventListener("transitionend", done);
animating = false;
});
}
function collapse() {
animating = true;
body.style.height = body.scrollHeight + "px";
body.style.opacity = "1";
requestAnimationFrame(() => {
body.style.transition = "height 0.28s ease, opacity 0.28s ease";
body.style.height = "0px";
body.style.opacity = "0";
});
body.addEventListener("transitionend", function done(ev) {
if (ev.propertyName !== "height") return;
step.removeAttribute("open");
body.style.transition = body.style.height = body.style.opacity = "";
body.removeEventListener("transitionend", done);
animating = false;
});
}
});
/* ---------- favorite (persisted to localStorage) ---------- */
const FAV_KEY = "stealthis:diy-project-detail:fav";
const favBtn = document.querySelector("[data-fav]");
const favLabel = document.querySelector("[data-fav-label]");
const favCount = document.querySelector("[data-fav-count]");
const BASE_FAVS = 9312;
function renderFav(on, announce) {
favBtn.setAttribute("aria-pressed", String(on));
favLabel.textContent = on ? "Favorited" : "Favorite";
favCount.textContent = (BASE_FAVS + (on ? 1 : 0)).toLocaleString("en-US");
if (announce) {
favBtn.classList.remove("pop");
void favBtn.offsetWidth; // restart animation
favBtn.classList.add("pop");
toast(on ? "Added to your favorites" : "Removed from favorites");
}
}
let favOn = false;
try {
favOn = localStorage.getItem(FAV_KEY) === "1";
} catch (_) {
/* storage unavailable (sandboxed iframe) — degrade gracefully */
}
renderFav(favOn, false);
favBtn.addEventListener("click", () => {
favOn = !favOn;
try {
favOn
? localStorage.setItem(FAV_KEY, "1")
: localStorage.removeItem(FAV_KEY);
} catch (_) {}
renderFav(favOn, true);
});
/* ---------- follow button ---------- */
const followBtn = document.querySelector("[data-follow]");
followBtn.addEventListener("click", () => {
const on = followBtn.classList.toggle("is-following");
followBtn.textContent = on ? "Following" : "Follow";
toast(on ? "Now following TinkerRosa" : "Unfollowed TinkerRosa");
});
/* ---------- comment replies + likes ---------- */
document.querySelectorAll("[data-reply]").forEach((btn) => {
btn.addEventListener("click", () =>
toast(`Replying to ${btn.dataset.reply} — editor coming right up (demo)`)
);
});
document.querySelectorAll("[data-like]").forEach((btn) => {
const n = btn.querySelector("[data-like-n]");
btn.addEventListener("click", () => {
const liked = btn.classList.toggle("is-liked");
n.textContent = String(parseInt(n.textContent, 10) + (liked ? 1 : -1));
});
});
/* ---------- comment form ---------- */
const cmtForm = document.querySelector("[data-cmt-form]");
const cmtInput = cmtForm.querySelector("input");
cmtForm.addEventListener("submit", (e) => {
e.preventDefault();
const text = cmtInput.value.trim();
if (!text) {
toast("Write something first — even a quick question helps!");
cmtInput.focus();
return;
}
cmtInput.value = "";
toast("Comment posted — thanks for joining the build thread!");
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mason Jar Herb Garden with Auto-Watering — MakerForge</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- ============ HERO ============ -->
<header class="hero" role="banner">
<div class="hero-inner">
<nav class="crumbs" aria-label="Breadcrumb">
<a href="#">MakerForge</a>
<span aria-hidden="true">/</span>
<a href="#">Garden & Outdoors</a>
<span aria-hidden="true">/</span>
<span aria-current="page">Auto-Watering</span>
</nav>
<p class="project-no">PROJECT NO. <span>MF-2214-HG</span></p>
<h1 class="hero-title">Mason Jar Herb Garden<br /><em>with Auto-Watering</em></h1>
<div class="hero-badges">
<span class="badge badge-difficulty" title="Difficulty">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M2 12h3V6H2v6Zm4.5 0h3V2h-3v10ZM11 12h3V8h-3v4Z"/></svg>
Intermediate
</span>
<span class="badge badge-time" title="Estimated build time">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1Zm0 12.5A5.5 5.5 0 1 1 8 2.5a5.5 5.5 0 0 1 0 11ZM8.75 4.5h-1.5v4l3.1 1.9.78-1.28-2.38-1.45V4.5Z"/></svg>
Weekend · ~5 hrs
</span>
<span class="badge badge-cost" title="Estimated cost">
<span class="mono">≈ $38</span> in parts
</span>
</div>
<div class="hero-meta">
<div class="author-chip">
<span class="avatar avatar-hero" aria-hidden="true">RQ</span>
<div class="author-txt">
<a href="#" class="author-name">Rosa "TinkerRosa" Quimby</a>
<span class="author-sub">Workshop Elder · 47 projects</span>
</div>
<button type="button" class="btn-follow" data-follow>Follow</button>
</div>
<div class="hero-stats" aria-label="Project stats">
<span class="stat"><strong class="mono">128,406</strong> views</span>
<span class="stat"><strong class="mono" data-fav-count>9,312</strong> favorites</span>
<span class="stat"><strong class="mono">214</strong> makes</span>
</div>
</div>
</div>
<div class="hero-figure" aria-hidden="true">
<div class="fig-frame">
<div class="fig-art">
<span class="fig-jar j1"></span>
<span class="fig-jar j2"></span>
<span class="fig-jar j3"></span>
<span class="fig-tube"></span>
<span class="fig-label mono">FIG. 01 — TRIPLE JAR ARRAY</span>
</div>
<span class="fig-corner tl"></span><span class="fig-corner tr"></span>
<span class="fig-corner bl"></span><span class="fig-corner br"></span>
</div>
</div>
</header>
<main class="page" id="top">
<!-- ============ INTRO ============ -->
<section class="intro card" aria-labelledby="intro-h">
<h2 id="intro-h" class="section-h">The Idea</h2>
<p>
My basil kept dying every time I left town, so I built a countertop herb garden that waters
itself. Three wide-mouth mason jars sit on a scrap-plywood cradle, fed by a tiny 5V
peristaltic pump on a moisture-sensor trigger. When the soil dries below the threshold, the
pump sips water from a hidden reservoir jar and drips exactly 40 ml into each planter.
Total draw is under 1 watt, and the whole rig runs off an old USB phone charger.
</p>
<p>
You need basic soldering skills and about half a Saturday. Everything else — drilling glass,
flashing the microcontroller, calibrating the sensor — is covered step by step below. If you
can assemble flat-pack furniture, you can build this.
</p>
</section>
<!-- ============ MATERIALS ============ -->
<section class="card materials" aria-labelledby="mat-h">
<div class="materials-head">
<h2 id="mat-h" class="section-h">Materials & Tools</h2>
<span class="mat-progress mono" data-mat-progress>0 / 12 gathered</span>
</div>
<div class="mat-cols">
<div class="mat-group">
<h3 class="mat-group-h">Materials</h3>
<ul class="checklist" data-checklist>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Wide-mouth mason jars, 32 oz</span>
<span class="qty mono">×4</span>
<span class="part mono">JAR-32W</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Peristaltic pump, 5V DC</span>
<span class="qty mono">×1</span>
<span class="part mono">PMP-5V-04</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Capacitive soil moisture sensor</span>
<span class="qty mono">×3</span>
<span class="part mono">SNS-CAP12</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Microcontroller board (SproutDuino Nano)</span>
<span class="qty mono">×1</span>
<span class="part mono">MCU-SD-N1</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Silicone tubing, 4 mm ID</span>
<span class="qty mono">2 m</span>
<span class="part mono">TUB-4ID</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Scrap plywood, 12 mm</span>
<span class="qty mono">450×200</span>
<span class="part mono">PLY-12</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Potting mix + herb seedlings</span>
<span class="qty mono">×3</span>
<span class="part mono">GRN-KIT</span>
</label>
</li>
</ul>
</div>
<div class="mat-group">
<h3 class="mat-group-h">Tools</h3>
<ul class="checklist" data-checklist>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Drill + diamond hole saw</span>
<span class="qty mono">6 mm</span>
<span class="part mono">TL-DRL</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Soldering iron + rosin solder</span>
<span class="qty mono">40 W</span>
<span class="part mono">TL-SLD</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Jigsaw or handsaw</span>
<span class="qty mono">—</span>
<span class="part mono">TL-SAW</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Hot glue gun</span>
<span class="qty mono">—</span>
<span class="part mono">TL-GLU</span>
</label>
</li>
<li>
<label class="check-row">
<input type="checkbox" />
<span class="box" aria-hidden="true"></span>
<span class="check-txt">Safety glasses + gloves</span>
<span class="qty mono">×1</span>
<span class="part mono">TL-PPE</span>
</label>
</li>
</ul>
</div>
</div>
<p class="mat-note">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M8 1 1 14h14L8 1Zm.75 10.5h-1.5V13h1.5v-1.5Zm0-5h-1.5v4h1.5v-4Z"/></svg>
Drilling glass throws fine dust — wear eye protection and drill under a trickle of water.
</p>
</section>
<!-- ============ STEPS ============ -->
<section class="steps" aria-labelledby="steps-h">
<h2 id="steps-h" class="section-h steps-title">Build Steps</h2>
<details class="step card" open>
<summary>
<span class="step-no mono">STEP 01</span>
<span class="step-name">Cut the plywood cradle</span>
<span class="step-time mono">45 min</span>
<span class="chev" aria-hidden="true"></span>
</summary>
<div class="step-body">
<div class="step-inner">
<div class="step-fig" role="img" aria-label="Diagram of the plywood cradle with three jar cutouts">
<span class="fig-tag mono">FIG. 02 — CRADLE LAYOUT · 450 × 200 mm</span>
<div class="fig-holes"><i></i><i></i><i></i></div>
</div>
<p>
Mark three 92 mm circles on the plywood, centered 140 mm apart. Cut them with
a jigsaw so each jar drops in and hangs by its shoulder. Sand every edge to
120 grit — the cradle sits on a kitchen counter, so no splinters.
</p>
<p>
Screw two 40 mm feet strips underneath. That gap is where the tubing and sensor
wires will run, keeping the top surface clean.
</p>
<div class="callout tip">
<span class="callout-tag mono">TIP</span>
<p>No jigsaw? Most makerspaces will laser-cut 12 mm ply from a simple SVG — ask for “through-cut, 3 circles.”</p>
</div>
</div>
</div>
</details>
<details class="step card">
<summary>
<span class="step-no mono">STEP 02</span>
<span class="step-name">Drill the jar lids</span>
<span class="step-time mono">30 min</span>
<span class="chev" aria-hidden="true"></span>
</summary>
<div class="step-body">
<div class="step-inner">
<div class="step-fig" role="img" aria-label="Diagram of a jar lid with drip hole and sensor slot">
<span class="fig-tag mono">FIG. 03 — LID DRILL PLAN · Ø6 mm</span>
<div class="fig-lid"><i class="hole"></i><i class="slot"></i></div>
</div>
<p>
Each planter lid gets a 6 mm hole for the drip line and a 12 mm slot for the
moisture sensor. Metal lids drill easily with a step bit; go slow and deburr with a
round file so the silicone tube isn't nicked.
</p>
<p>
The reservoir jar keeps its lid solid except for one 6 mm intake hole — that's
what stops evaporation and keeps curious cats out of your water supply.
</p>
<div class="callout warn">
<span class="callout-tag mono">CAUTION</span>
<p>Clamp lids to a wood block before drilling. A spinning lid edge is basically a saw blade.</p>
</div>
</div>
</div>
</details>
<details class="step card">
<summary>
<span class="step-no mono">STEP 03</span>
<span class="step-name">Wire the pump & sensors</span>
<span class="step-time mono">75 min</span>
<span class="chev" aria-hidden="true"></span>
</summary>
<div class="step-body">
<div class="step-inner">
<div class="step-fig" role="img" aria-label="Wiring diagram from sensors and pump to the microcontroller">
<span class="fig-tag mono">FIG. 04 — WIRING · SNS-CAP12 ×3 → A0/A1/A2</span>
<div class="fig-wires"><i></i><i></i><i></i></div>
</div>
<p>
Solder the three sensors to analog pins <span class="mono pin">A0</span>,
<span class="mono pin">A1</span>, <span class="mono pin">A2</span>. The pump runs
through an NPN transistor on pin <span class="mono pin">D5</span> with a flyback diode
across the motor terminals — do not skip the diode, it protects the board from voltage
spikes when the pump stops.
</p>
<p>
Heat-shrink every joint. This rig lives next to water; there is no such thing as too
much insulation here.
</p>
<div class="callout tip">
<span class="callout-tag mono">TIP</span>
<p>Label each sensor lead with masking-tape flags (J1/J2/J3) now. Future-you, debugging at midnight, says thanks.</p>
</div>
</div>
</div>
</details>
<details class="step card">
<summary>
<span class="step-no mono">STEP 04</span>
<span class="step-name">Flash & calibrate</span>
<span class="step-time mono">40 min</span>
<span class="chev" aria-hidden="true"></span>
</summary>
<div class="step-body">
<div class="step-inner">
<div class="step-fig code-fig" aria-label="Firmware calibration constants">
<span class="fig-tag mono">FIRMWARE — thresholds.h</span>
<pre class="mono"><code>#define DRY_THRESHOLD 612 // raw ADC, air ≈ 830
#define WET_THRESHOLD 347 // raw ADC, water ≈ 300
#define DOSE_ML 40 // per watering event
#define COOLDOWN_MIN 90 // minutes between doses</code></pre>
</div>
<p>
Flash the firmware, then calibrate: note each sensor's reading in dry air and in a
glass of water, and set your thresholds about 15% inside those extremes. Run the pump
into a measuring cup for 10 seconds to compute your ml-per-second rate.
</p>
<div class="callout tip">
<span class="callout-tag mono">TIP</span>
<p>Capacitive sensors drift for the first day in soil. Re-check thresholds after 24 hours before trusting the automation.</p>
</div>
</div>
</div>
</details>
<details class="step card">
<summary>
<span class="step-no mono">STEP 05</span>
<span class="step-name">Plant, tube & test</span>
<span class="step-time mono">50 min</span>
<span class="chev" aria-hidden="true"></span>
</summary>
<div class="step-body">
<div class="step-inner">
<div class="step-fig" role="img" aria-label="Final assembly with tubing manifold feeding three jars">
<span class="fig-tag mono">FIG. 05 — FINAL ASSEMBLY · MANIFOLD SPLIT 1→3</span>
<div class="fig-final"><i></i><i></i><i></i><b></b></div>
</div>
<p>
Layer each jar: 3 cm of gravel, then potting mix, then your seedling. Basil,
chives, and mint are forgiving starters. Route the tubing from pump to a 1→3 manifold
under the cradle, push each line through its lid hole, and seat the sensors halfway
down the soil column.
</p>
<p>
Fill the reservoir, plug in the USB charger, and force a test cycle with the button on
<span class="mono pin">D2</span>. You should hear the pump purr and see three slow
drips. That's it — walk away for a week and come back to happy herbs.
</p>
<div class="callout warn">
<span class="callout-tag mono">CAUTION</span>
<p>Keep the USB supply and all connectors above reservoir level so any drip runs away from the electronics.</p>
</div>
</div>
</div>
</details>
</section>
<!-- ============ I MADE IT ============ -->
<section class="made card" aria-labelledby="made-h">
<div class="made-head">
<h2 id="made-h" class="section-h">I Made It!</h2>
<button type="button" class="btn btn-outline" data-toast="Upload flow is illustrative — this demo has no backend">
+ Share your build
</button>
</div>
<div class="made-grid">
<article class="make-card">
<div class="make-img mk-1" aria-hidden="true"><span class="mono">MAKE #214</span></div>
<div class="make-meta">
<span class="avatar av-g">FD</span>
<div>
<span class="make-name">Femi "DrillBit" Dawodu</span>
<p class="make-cap">Swapped mint for cilantro and doubled the reservoir. Two weeks unattended, zero casualties.</p>
</div>
</div>
</article>
<article class="make-card">
<div class="make-img mk-2" aria-hidden="true"><span class="mono">MAKE #198</span></div>
<div class="make-meta">
<span class="avatar av-b">LP</span>
<div>
<span class="make-name">Lena Parkhurst</span>
<p class="make-cap">Built the cradle from an old skateboard deck. The concave actually helps the tubes route!</p>
</div>
</div>
</article>
<article class="make-card">
<div class="make-img mk-3" aria-hidden="true"><span class="mono">MAKE #171</span></div>
<div class="make-meta">
<span class="avatar av-o">MT</span>
<div>
<span class="make-name">Marco "SolderMarco" Trevi</span>
<p class="make-cap">Added a tiny OLED showing soil % per jar. Firmware fork linked in my profile.</p>
</div>
</div>
</article>
<article class="make-card">
<div class="make-img mk-4" aria-hidden="true"><span class="mono">MAKE #156</span></div>
<div class="make-meta">
<span class="avatar av-k">AY</span>
<div>
<span class="make-name">Aki Yoshinaga</span>
<p class="make-cap">Five-jar version for a classroom windowsill. Kids named the pump "Gulper."</p>
</div>
</div>
</article>
</div>
</section>
<!-- ============ COMMENTS ============ -->
<section class="comments card" aria-labelledby="cmt-h">
<h2 id="cmt-h" class="section-h">Comments <span class="cmt-count mono">(37)</span></h2>
<ol class="cmt-list">
<li class="cmt">
<span class="avatar av-b">HB</span>
<div class="cmt-body">
<div class="cmt-top">
<span class="cmt-name">Hattie Broadmoor</span>
<span class="cmt-when mono">2 days ago</span>
</div>
<p>Would a 3.3V board work instead of 5V? I have a drawer full of SproutDuino Minis and zero patience for shipping.</p>
<div class="cmt-actions">
<button type="button" class="cmt-btn" data-reply="Hattie Broadmoor">Reply</button>
<button type="button" class="cmt-btn cmt-like" data-like>▲ <span class="mono" data-like-n>14</span></button>
</div>
</div>
</li>
<li class="cmt cmt-author">
<span class="avatar avatar-hero">RQ</span>
<div class="cmt-body">
<div class="cmt-top">
<span class="cmt-name">TinkerRosa <span class="author-tag mono">AUTHOR</span></span>
<span class="cmt-when mono">2 days ago</span>
</div>
<p>Yes — the sensors are happy at 3.3V, but run the pump from the 5V USB rail directly through the transistor. Don't feed the motor from the board's regulator.</p>
<div class="cmt-actions">
<button type="button" class="cmt-btn" data-reply="TinkerRosa">Reply</button>
<button type="button" class="cmt-btn cmt-like" data-like>▲ <span class="mono" data-like-n>22</span></button>
</div>
</div>
</li>
<li class="cmt">
<span class="avatar av-o">GN</span>
<div class="cmt-body">
<div class="cmt-top">
<span class="cmt-name">Gus Nordvik</span>
<span class="cmt-when mono">5 days ago</span>
</div>
<p>Built this over the weekend. The cooldown constant saved me — first attempt without it turned my basil jar into a swamp in an hour.</p>
<div class="cmt-actions">
<button type="button" class="cmt-btn" data-reply="Gus Nordvik">Reply</button>
<button type="button" class="cmt-btn cmt-like" data-like>▲ <span class="mono" data-like-n>31</span></button>
</div>
</div>
</li>
<li class="cmt">
<span class="avatar av-g">PV</span>
<div class="cmt-body">
<div class="cmt-top">
<span class="cmt-name">Priya Venkatesan</span>
<span class="cmt-when mono">1 week ago</span>
</div>
<p>Any reason not to use one long sensor across all three jars? Asking before I order three of them.</p>
<div class="cmt-actions">
<button type="button" class="cmt-btn" data-reply="Priya Venkatesan">Reply</button>
<button type="button" class="cmt-btn cmt-like" data-like>▲ <span class="mono" data-like-n>8</span></button>
</div>
</div>
</li>
</ol>
<form class="cmt-form" data-cmt-form>
<label class="sr-only" for="cmt-input">Add a comment</label>
<input id="cmt-input" type="text" placeholder="Ask a question or share a tip…" autocomplete="off" />
<button type="submit" class="btn btn-solid">Post</button>
</form>
</section>
</main>
<!-- ============ STICKY ACTION BAR ============ -->
<div class="action-bar" role="toolbar" aria-label="Project actions">
<button type="button" class="act act-fav" data-fav aria-pressed="false">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path class="heart" d="M12 21s-7.5-4.9-10-9.6C.4 8.4 2.4 4.6 6 4.1c2-.3 3.9.7 6 3 2.1-2.3 4-3.3 6-3 3.6.5 5.6 4.3 4 7.3-2.5 4.7-10 9.6-10 9.6Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
<span data-fav-label>Favorite</span>
</button>
<button type="button" class="act" data-toast="Link copied to clipboard — paste it anywhere">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M18 16a3 3 0 0 0-2.24 1.02l-7-4.07a3.05 3.05 0 0 0 0-1.9l7-4.07A3 3 0 1 0 15 5a3 3 0 0 0 .05.51l-7 4.07a3 3 0 1 0 0 4.84l7 4.07A3 3 0 1 0 18 16Z"/></svg>
Share
</button>
<button type="button" class="act act-cta" data-toast="Generating PDF plan set… check your downloads (demo)">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 16 6 10h4V3h4v7h4l-6 6Zm-7 4h14v-2H5v2Z"/></svg>
Download PDF
</button>
</div>
<div class="toast-region" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Project Detail
A full maker-community project page in the workshop/blueprint aesthetic. The navy hero carries the signature 8px blueprint grid, a stencil-style uppercase title with a mono project number, difficulty/time/cost badges, an author chip with a working follow toggle, and a dashed technical figure of the triple-jar array. Below it, the intro card and a materials & tools checklist track gathering progress live — checking all twelve items flips the counter to green and fires a celebratory toast.
The five build steps are details/summary chapters enhanced with a JS height-animated expand/collapse, each holding a blueprint diagram area, realistic instructions with mono pin badges, and TIP/CAUTION callouts. An “I Made It!” grid shows four community builds with avatars and captions, followed by a comment thread with like counters, reply buttons, and a working comment form.
The sticky bottom action bar floats over everything: the favorite heart persists to localStorage (and bumps the hero favorite count), while share and download-PDF surface toasts from a small reusable toast(msg) helper.
Illustrative UI only — always follow real safety guidance when working with tools, electronics, or repairs.