DIY — Woodworking Shop Landing
A warm kraft-paper landing page for a fictional woodworking shop and school, built with plain HTML, CSS and vanilla JavaScript. It pairs a self-drawing SVG bench cut-plan with dimension lines, a row of isometric project cards carrying skill badges and board-feet chips, a live cut-list calculator that prices stock by species, a hoverable tool strip with line-art icons, a wood-species selector that recolors a CSS grain preview, workshop cards with seats-left meters, and a newsletter footer.
MCP
Code
:root{
--kraft:#efe6d4;
--cream:#f7f1e4;
--walnut:#3b2a1d;
--walnut-2:#5b4433;
--oak:#a97f4f;
--oak-l:#c9ab84;
--burnt:#c25b2b;
--burnt-d:#a2461f;
--sage:#6b7f56;
--muted:#7c6a58;
--line:rgba(59,42,29,.16);
--line-2:rgba(59,42,29,.30);
--ok:#6b7f56;
--warn:#c98a2b;
--danger:#b1442c;
--r-sm:6px; --r-md:12px; --r-lg:18px;
--sh-1:0 1px 2px rgba(59,42,29,.07), 0 6px 18px rgba(59,42,29,.07);
--sh-2:0 2px 4px rgba(59,42,29,.09), 0 18px 44px rgba(59,42,29,.13);
--ui:'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;
--mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',monospace;
--serif:'Inter',Georgia,serif;
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;
font-family:var(--ui);
line-height:1.5;
color:var(--walnut);
background:var(--kraft);
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
/* kraft paper fiber */
background-image:
repeating-linear-gradient(92deg, rgba(169,127,79,.055) 0 1px, transparent 1px 4px),
repeating-linear-gradient(3deg, rgba(59,42,29,.035) 0 1px, transparent 1px 7px),
radial-gradient(120% 80% at 20% 0%, rgba(247,241,228,.9), transparent 60%);
}
img,svg{max-width:100%}
h1,h2,h3{margin:0 0 .4em;line-height:1.12;letter-spacing:-.02em;font-weight:800}
p{margin:0 0 1em}
.mono{font-family:var(--mono);font-variant-ligatures:none}
.accent{color:var(--burnt)}
.wrap{width:min(1160px,100% - 2.5rem);margin-inline:auto}
.sr,.skip{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.skip:focus{position:fixed;top:10px;left:10px;width:auto;height:auto;clip:auto;padding:.6rem 1rem;background:var(--walnut);color:var(--cream);border-radius:var(--r-sm);z-index:99}
:focus-visible{outline:2.5px solid var(--burnt);outline-offset:3px;border-radius:4px}
/* ---------- buttons ---------- */
.btn{
--b:var(--burnt);
display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
font:600 .95rem/1 var(--ui);
padding:.85rem 1.35rem;border:1.5px solid var(--b);border-radius:var(--r-sm);
background:var(--b);color:var(--cream);cursor:pointer;text-decoration:none;
box-shadow:0 2px 0 var(--burnt-d);
transition:transform .12s ease, box-shadow .12s ease, background .18s ease;
}
.btn:hover{background:var(--burnt-d);transform:translateY(-1px);box-shadow:0 3px 0 var(--burnt-d)}
.btn:active{transform:translateY(1px);box-shadow:0 0 0 var(--burnt-d)}
.btn-ghost{background:transparent;color:var(--walnut);border-color:var(--line-2);box-shadow:none}
.btn-ghost:hover{background:rgba(169,127,79,.14);color:var(--walnut);box-shadow:none}
.btn-sm{padding:.55rem .95rem;font-size:.85rem}
/* ---------- nav ---------- */
.nav{position:sticky;top:0;z-index:40;background:rgba(239,230,212,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.nav-in{display:flex;align-items:center;gap:1.5rem;padding:.75rem 0}
.brand{display:flex;align-items:center;gap:.6rem;color:var(--walnut);text-decoration:none;font-weight:800;letter-spacing:-.02em}
.brand em{font-style:normal;font-weight:500;color:var(--oak);margin-left:.35rem}
.brand-mark{width:30px;height:30px;color:var(--burnt);flex:none}
.nav nav{margin-left:auto;display:flex;gap:1.4rem}
.nav nav a{color:var(--walnut-2);text-decoration:none;font-size:.92rem;font-weight:500;padding:.25rem 0;border-bottom:2px solid transparent}
.nav nav a:hover{color:var(--burnt);border-bottom-color:var(--burnt)}
/* ---------- hero ---------- */
.hero{padding:clamp(3rem,7vw,5.5rem) 0 clamp(2.5rem,5vw,4rem);position:relative;overflow:hidden}
.hero::after{content:"";position:absolute;inset:auto 0 0 0;height:2px;background:repeating-linear-gradient(90deg,var(--line-2) 0 10px,transparent 10px 20px)}
.hero-in{display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(2rem,5vw,4rem);align-items:center}
.eyebrow{display:inline-block;font:600 .72rem/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--oak);border:1px dashed var(--line-2);padding:.5rem .75rem;border-radius:var(--r-sm);margin-bottom:1.25rem}
h1{font-family:var(--serif);font-size:clamp(2.15rem,5.4vw,3.6rem)}
h1 em{font-style:italic;font-weight:800;color:var(--burnt)}
.lede{font-size:1.06rem;color:var(--walnut-2);max-width:46ch}
.cta-row{display:flex;flex-wrap:wrap;gap:.75rem;margin:1.6rem 0 2rem}
.hero-stats{display:flex;gap:2.25rem;margin:0;padding-top:1.25rem;border-top:1px solid var(--line)}
.hero-stats dt{font:500 .72rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:.35rem}
.hero-stats dd{margin:0;font:700 1.55rem/1 var(--mono);color:var(--walnut)}
.plan{margin:0;background:var(--cream);border:1px solid var(--line);border-radius:var(--r-lg);padding:1.25rem 1.25rem 1rem;box-shadow:var(--sh-2);position:relative}
.plan::before{content:"";position:absolute;inset:.5rem;border:1px dashed var(--line-2);border-radius:calc(var(--r-lg) - 6px);pointer-events:none}
.plan svg{display:block;color:var(--walnut);width:100%;height:auto}
.plan .dim{color:var(--burnt);opacity:.9}
.dimtext{font-family:var(--mono);font-size:11px;font-weight:600;fill:var(--burnt);letter-spacing:.06em}
.plan figcaption{font:500 .78rem/1.4 var(--mono);color:var(--muted);text-align:center;padding-top:.6rem;border-top:1px dashed var(--line-2);margin-top:.4rem}
.plan .draw path,.plan .draw circle,.plan .dim path{
stroke-dasharray:var(--len,600);stroke-dashoffset:var(--len,600);
}
.plan.drawn .draw path,.plan.drawn .draw circle,.plan.drawn .dim path{
animation:ink 1.5s cubic-bezier(.6,.05,.3,1) forwards;
}
@keyframes ink{to{stroke-dashoffset:0}}
/* ---------- sections ---------- */
.band{padding:clamp(3rem,6vw,4.75rem) 0}
.band.alt{background:linear-gradient(180deg,rgba(247,241,228,.85),rgba(247,241,228,.5));border-block:1px solid var(--line)}
.sec-head{max-width:52ch;margin-bottom:2rem}
.sec-head h2{font-family:var(--serif);font-size:clamp(1.6rem,3.4vw,2.25rem)}
.sec-head p{color:var(--muted);margin:0}
.sub{color:var(--muted);font-size:.92rem}
/* ---------- project cards ---------- */
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:1.1rem}
.card{
background:var(--cream);border:1px solid var(--line);border-radius:var(--r-md);
padding:1.1rem;box-shadow:var(--sh-1);
transition:transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.card:hover{transform:translateY(-4px);box-shadow:var(--sh-2);border-color:var(--oak)}
.iso{background:
repeating-linear-gradient(45deg,rgba(169,127,79,.09) 0 2px,transparent 2px 9px),
var(--kraft);
border:1px dashed var(--line-2);border-radius:var(--r-sm);padding:.6rem;margin-bottom:.9rem;color:var(--walnut-2)}
.iso svg{display:block;width:100%;height:auto;transition:transform .3s ease}
.card:hover .iso svg{transform:translateY(-3px) scale(1.03)}
.card h3{font-size:1.08rem}
.card p{font-size:.88rem;color:var(--muted);margin-bottom:.85rem}
.chips{display:flex;flex-wrap:wrap;gap:.4rem}
.chip{font:600 .72rem/1 var(--mono);padding:.4rem .55rem;border-radius:var(--r-sm);background:rgba(169,127,79,.16);color:var(--walnut-2)}
.badge{font:700 .68rem/1 var(--ui);letter-spacing:.06em;text-transform:uppercase;padding:.42rem .6rem;border-radius:var(--r-sm);color:#fff}
.lvl-1{background:var(--sage)}
.lvl-2{background:var(--warn);color:#2c1f10}
.lvl-3{background:var(--burnt)}
/* ---------- split: calc + tools ---------- */
.split{display:grid;grid-template-columns:1fr 1fr;gap:clamp(1.5rem,4vw,3rem);align-items:start}
.calc{background:var(--cream);border:1px solid var(--line);border-radius:var(--r-lg);padding:1.5rem;box-shadow:var(--sh-1)}
.calc h2{font-family:var(--serif);font-size:1.45rem}
.field{display:grid;gap:.35rem;margin-bottom:.85rem}
.field label{font:600 .78rem/1 var(--ui);color:var(--walnut-2)}
.field label .mono{color:var(--muted);font-weight:400}
input,select{
font:500 .95rem/1 var(--mono);color:var(--walnut);
padding:.7rem .75rem;border:1.5px solid var(--line-2);border-radius:var(--r-sm);
background:var(--kraft);width:100%;
}
input:focus,select:focus{border-color:var(--burnt);background:#fff}
input:invalid{border-color:var(--danger)}
.readout{
display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem;
margin:1.1rem 0;padding:.95rem;border-radius:var(--r-md);
background:var(--walnut);color:var(--cream);
}
.readout .k{display:block;font:500 .68rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;opacity:.65;margin-bottom:.4rem}
.readout strong{font-size:1.15rem;font-weight:700}
.readout .accent{color:#f0a077}
.tools h2{font-family:var(--serif);font-size:1.45rem}
.tool-strip{list-style:none;margin:0 0 1rem;padding:0;display:grid;grid-template-columns:repeat(4,1fr);gap:.6rem}
.tool-strip li{
background:var(--cream);border:1px solid var(--line);border-radius:var(--r-md);
padding:.85rem .5rem;text-align:center;cursor:pointer;color:var(--walnut-2);
transition:background .18s ease,border-color .18s ease,transform .18s ease;
}
.tool-strip li:hover,.tool-strip li:focus-visible,.tool-strip li.on{background:var(--walnut);color:var(--cream);border-color:var(--walnut);transform:translateY(-3px)}
.tool-strip svg{display:block;margin:0 auto .45rem;width:100%;max-width:58px;height:auto}
.tool-strip span{font:600 .74rem/1.2 var(--ui);display:block}
.tool-note{
min-height:3.2em;font-size:.82rem;color:var(--walnut-2);
border-left:3px solid var(--burnt);padding:.6rem .8rem;background:rgba(169,127,79,.1);border-radius:0 var(--r-sm) var(--r-sm) 0;margin:0;
}
/* ---------- species ---------- */
.species{display:grid;grid-template-columns:220px 1fr;gap:1.5rem;align-items:start}
.swatches{display:grid;gap:.55rem}
.sw{
font:700 .95rem/1 var(--ui);text-align:left;cursor:pointer;
padding:.9rem 1rem;border-radius:var(--r-sm);border:1.5px solid var(--line-2);
background:var(--cream);color:var(--walnut);position:relative;padding-left:2.9rem;
transition:border-color .16s ease,background .16s ease,transform .16s ease;
}
.sw::before{content:"";position:absolute;left:.85rem;top:50%;translate:0 -50%;width:22px;height:22px;border-radius:5px;border:1px solid rgba(59,42,29,.28)}
.sw[data-species=pine]::before{background:linear-gradient(135deg,#e6cfa4,#c9ac7c)}
.sw[data-species=oak]::before{background:linear-gradient(135deg,#d9b184,#b5854f)}
.sw[data-species=walnut]::before{background:linear-gradient(135deg,#8a6244,#4f3524)}
.sw[data-species=maple]::before{background:linear-gradient(135deg,#f0dfbd,#d3ba8d)}
.sw:hover{transform:translateX(3px);border-color:var(--oak)}
.sw[aria-checked=true]{border-color:var(--burnt);background:#fff;box-shadow:inset 3px 0 0 var(--burnt)}
.preview{display:grid;grid-template-columns:1fr 1fr;border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--sh-1);background:var(--cream);min-height:260px}
.grain{
--t:#8a6244; --t2:#4f3524;
background-color:var(--t);
background-image:
repeating-linear-gradient(97deg, var(--t2) 0 1px, transparent 1px 6px),
repeating-linear-gradient(93deg, rgba(255,255,255,.16) 0 2px, transparent 2px 13px),
repeating-linear-gradient(90deg, rgba(0,0,0,.10) 0 3px, transparent 3px 27px);
transition:background-color .45s ease;
}
.preview-body{padding:1.4rem}
.preview-body h3{font-family:var(--serif);font-size:1.35rem}
.preview-body p{font-size:.9rem;color:var(--muted)}
.spstats{display:grid;gap:.7rem;margin:0 0 1rem}
.spstats dt{font:500 .68rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.spstats dd{margin:.25rem 0 0;font-size:1.15rem;font-weight:700}
.meter{height:9px;border-radius:99px;background:rgba(59,42,29,.13);overflow:hidden}
.meter span{display:block;height:100%;width:0;border-radius:99px;background:linear-gradient(90deg,var(--oak),var(--burnt));transition:width .5s cubic-bezier(.3,.9,.3,1)}
/* ---------- classes ---------- */
.classes{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1.1rem}
.klass{background:var(--cream);border:1px solid var(--line);border-radius:var(--r-md);padding:1.25rem;box-shadow:var(--sh-1);display:flex;flex-direction:column}
.klass h3{font-size:1.1rem}
.klass p{font-size:.88rem;color:var(--muted)}
.tag{display:inline-block;font-size:.7rem;font-weight:700;letter-spacing:.1em;color:var(--sage);margin-bottom:.6rem}
.seats{margin:auto 0 1rem}
.seats .bar{height:7px;border-radius:99px;background:rgba(59,42,29,.13);overflow:hidden;margin-bottom:.4rem}
.seats .bar span{display:block;height:100%;background:var(--sage);transition:width .45s ease}
.seats.low .bar span{background:var(--burnt)}
.seats small{color:var(--muted);font-size:.74rem}
.klass.full{opacity:.72}
.klass.full .btn{pointer-events:none}
/* ---------- quote ---------- */
.quote-band{padding-block:clamp(2.5rem,5vw,4rem)}
blockquote{margin:0;max-width:62ch;border-left:4px solid var(--burnt);padding-left:1.4rem}
blockquote p{font-family:var(--serif);font-size:clamp(1.1rem,2.4vw,1.5rem);font-style:italic;color:var(--walnut)}
blockquote footer{font-size:.85rem;color:var(--muted)}
/* ---------- footer ---------- */
.foot{background:var(--walnut);color:var(--cream);padding:clamp(2.5rem,5vw,3.75rem) 0 2rem;
background-image:repeating-linear-gradient(95deg,rgba(255,255,255,.035) 0 1px,transparent 1px 7px)}
.foot-in{display:grid;grid-template-columns:1.3fr .7fr;gap:2rem}
.foot h2{font-family:var(--serif);font-size:1.5rem}
.foot p{color:rgba(247,241,228,.72);font-size:.92rem}
.news{display:flex;gap:.6rem;max-width:420px;margin-top:1rem}
.news input{background:rgba(255,255,255,.07);border-color:rgba(247,241,228,.28);color:var(--cream)}
.news input::placeholder{color:rgba(247,241,228,.45)}
.news input:focus{background:rgba(255,255,255,.14);border-color:var(--burnt)}
.err{color:#f0a077;font-size:.8rem;min-height:1.2em;margin:.4rem 0 0}
.foot-meta{text-align:right}
.foot-meta .mono{font-size:.78rem;letter-spacing:.05em;line-height:1.8}
.fine{font-size:.74rem;opacity:.55}
/* ---------- toast ---------- */
.toast{
position:fixed;left:50%;bottom:1.5rem;translate:-50% 120%;
background:var(--walnut);color:var(--cream);border:1px solid var(--oak);
padding:.8rem 1.15rem;border-radius:var(--r-sm);font-size:.88rem;font-weight:600;
box-shadow:var(--sh-2);z-index:80;opacity:0;transition:translate .35s cubic-bezier(.2,.9,.3,1),opacity .3s ease;
max-width:min(90vw,420px);text-align:center;
}
.toast.show{translate:-50% 0;opacity:1}
/* ---------- responsive ---------- */
@media (max-width:920px){
.hero-in,.split,.species,.preview,.foot-in{grid-template-columns:1fr}
.foot-meta{text-align:left}
.species .preview{min-height:0}
.grain{min-height:120px}
.nav nav{display:none}
}
@media (max-width:520px){
.wrap{width:min(1160px,100% - 1.75rem)}
.hero-stats{gap:1.25rem;flex-wrap:wrap}
.hero-stats dd{font-size:1.25rem}
.tool-strip{grid-template-columns:repeat(2,1fr)}
.readout{grid-template-columns:1fr;gap:.5rem;text-align:left}
.news{flex-direction:column;max-width:none}
.cta-row .btn{flex:1 1 100%}
.swatches{grid-template-columns:repeat(2,1fr)}
.brand em{display:none}
}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important}
.plan .draw path,.plan .draw circle,.plan .dim path{stroke-dashoffset:0 !important}
}(function () {
"use strict";
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- toast ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 3200);
}
/* ---------- hero plan: self-drawing SVG ---------- */
var plan = document.querySelector(".plan");
var svg = document.getElementById("planSvg");
if (svg) {
var strokes = svg.querySelectorAll(".draw path, .draw circle, .dim path");
Array.prototype.forEach.call(strokes, function (el, i) {
var len = 400;
try { len = Math.ceil(el.getTotalLength()) || 400; } catch (e) { /* jsdom */ }
el.style.setProperty("--len", len);
el.style.animationDelay = (reduced ? 0 : i * 55) + "ms";
});
if (reduced) {
plan.classList.add("drawn");
} else if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { plan.classList.add("drawn"); io.disconnect(); }
});
}, { threshold: 0.25 });
io.observe(plan);
} else {
plan.classList.add("drawn");
}
}
/* ---------- species state (shared with calculator) ---------- */
var species = {
name: "Black Walnut",
price: 14.6,
hard: 1010
};
var MAX_JANKA = 1600;
var swatches = document.querySelectorAll(".sw");
var grain = document.getElementById("grain");
var spName = document.getElementById("spName");
var spDesc = document.getElementById("spDesc");
var spHard = document.getElementById("spHard");
var spPrice = document.getElementById("spPrice");
var hardBar = document.getElementById("hardBar");
var outSpecies = document.getElementById("outSpecies");
function selectSpecies(btn, announce) {
Array.prototype.forEach.call(swatches, function (b) {
b.setAttribute("aria-checked", String(b === btn));
b.tabIndex = b === btn ? 0 : -1;
});
species.name = btn.dataset.label;
species.price = parseFloat(btn.dataset.price);
species.hard = parseInt(btn.dataset.hard, 10);
if (grain) {
grain.style.setProperty("--t", btn.dataset.tone);
grain.style.setProperty("--t2", btn.dataset.tone2);
}
spName.textContent = species.name;
spDesc.textContent = btn.dataset.desc;
spHard.textContent = species.hard + " lbf";
spPrice.textContent = "$" + species.price.toFixed(2);
hardBar.style.width = Math.min(100, (species.hard / MAX_JANKA) * 100).toFixed(1) + "%";
hardBar.parentNode.setAttribute("aria-label", "Janka hardness " + species.hard + " of " + MAX_JANKA);
if (outSpecies) outSpecies.textContent = species.name.split(" ").pop();
compute();
if (announce) toast(species.name + " · $" + species.price.toFixed(2) + " per board foot");
}
Array.prototype.forEach.call(swatches, function (btn, i) {
btn.tabIndex = btn.getAttribute("aria-checked") === "true" ? 0 : -1;
btn.addEventListener("click", function () { selectSpecies(btn, true); });
btn.addEventListener("keydown", function (e) {
var d = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1
: e.key === "ArrowLeft" || e.key === "ArrowUp" ? -1 : 0;
if (!d) return;
e.preventDefault();
var next = swatches[(i + d + swatches.length) % swatches.length];
next.focus();
selectSpecies(next, true);
});
});
/* ---------- cut-list calculator ---------- */
var form = document.getElementById("calcForm");
var wEl = document.getElementById("w");
var lEl = document.getElementById("l");
var qEl = document.getElementById("q");
var tEl = document.getElementById("thick");
var outBf = document.getElementById("outBf");
var outCost = document.getElementById("outCost");
function num(el, fallback) {
var v = parseFloat(el.value);
return isFinite(v) && v > 0 ? v : fallback;
}
function compute() {
if (!form) return 0;
var t = parseFloat(tEl.value);
var bf = (t * num(wEl, 0) * num(lEl, 0) * num(qEl, 0)) / 144;
outBf.textContent = bf ? bf.toFixed(2) : "—";
outCost.textContent = bf ? "$" + (bf * species.price).toFixed(2) : "—";
return bf;
}
if (form) {
["input", "change"].forEach(function (ev) {
form.addEventListener(ev, compute);
});
form.addEventListener("submit", function (e) {
e.preventDefault();
var bf = compute();
if (!bf) { toast("Enter a width, length and quantity first."); return; }
toast(
num(qEl, 1) + " × " + species.name + " — " + bf.toFixed(2) +
" bd ft added to your cut list ($" + (bf * species.price).toFixed(2) + ")"
);
});
}
/* ---------- tool strip ---------- */
var note = document.getElementById("toolNote");
var tools = document.querySelectorAll(".tool-strip li");
function showNote(li) {
Array.prototype.forEach.call(tools, function (t) { t.classList.toggle("on", t === li); });
note.textContent = li.dataset.note;
}
Array.prototype.forEach.call(tools, function (li) {
li.addEventListener("mouseenter", function () { showNote(li); });
li.addEventListener("focus", function () { showNote(li); });
li.addEventListener("click", function () { showNote(li); });
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); showNote(li); }
});
});
/* ---------- classes: seat meters ---------- */
Array.prototype.forEach.call(document.querySelectorAll(".klass"), function (card) {
var seats = parseInt(card.dataset.seats, 10);
var cap = parseInt(card.dataset.cap, 10);
var box = card.querySelector(".seats");
var bar = box.querySelector("span");
var label = box.querySelector("small");
var btn = card.querySelector(".book");
function render() {
var taken = cap - seats;
bar.style.width = ((taken / cap) * 100).toFixed(1) + "%";
box.classList.toggle("low", seats > 0 && seats <= 2);
label.textContent = seats > 0
? seats + " of " + cap + " seats left"
: "Waitlist only";
if (seats === 0) {
card.classList.add("full");
btn.textContent = "Join waitlist";
btn.disabled = true;
}
}
render();
btn.addEventListener("click", function () {
if (seats <= 0) return;
seats -= 1;
card.dataset.seats = seats;
render();
toast("Seat held for " + card.querySelector("h3").textContent + " — 15 minutes to confirm.");
});
});
/* ---------- newsletter ---------- */
var news = document.getElementById("newsForm");
var email = document.getElementById("email");
var err = document.getElementById("newsErr");
if (news) {
news.addEventListener("submit", function (e) {
e.preventDefault();
var ok = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.value.trim());
if (!ok) {
err.textContent = "That address doesn't look right — check it and try again.";
email.setAttribute("aria-invalid", "true");
email.focus();
return;
}
err.textContent = "";
email.removeAttribute("aria-invalid");
email.value = "";
toast("You're on the list. The Offcut ships the first Tuesday of the month.");
});
}
/* ---------- smooth anchors ---------- */
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
a.addEventListener("click", function (e) {
var target = document.querySelector(a.getAttribute("href"));
if (!target) return;
e.preventDefault();
target.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "start" });
});
});
compute();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Grainhouse Woodworks — Shop & School</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>
<a class="skip" href="#main">Skip to content</a>
<header class="nav">
<div class="wrap nav-in">
<a class="brand" href="#main">
<svg viewBox="0 0 40 40" aria-hidden="true" class="brand-mark">
<path d="M6 30 L20 8 L34 30 Z" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/>
<path d="M12 30 L20 17 L28 30" fill="none" stroke="currentColor" stroke-width="1.6" opacity=".55"/>
</svg>
<span>Grainhouse<em>Woodworks</em></span>
</a>
<nav aria-label="Primary">
<a href="#projects">Projects</a>
<a href="#calc">Cut list</a>
<a href="#species">Species</a>
<a href="#classes">Classes</a>
</nav>
<a class="btn btn-sm" href="#classes">Book a bench</a>
</div>
</header>
<main id="main">
<!-- HERO -->
<section class="hero">
<div class="wrap hero-in">
<div class="hero-copy">
<span class="eyebrow">Est. 2011 · Shop No. 4, Alder Street</span>
<h1>Measure twice.<br>Then build something<br><em>that outlives you.</em></h1>
<p class="lede">A working woodshop and open-bench school. Hand-cut joinery, kiln-dried
hardwood from within 200 miles, and eight benches you can rent by the evening.</p>
<div class="cta-row">
<a class="btn" href="#classes">Reserve a bench</a>
<a class="btn btn-ghost" href="#projects">See the build plans</a>
</div>
<dl class="hero-stats">
<div><dt>Benches</dt><dd>08</dd></div>
<div><dt>Species stocked</dt><dd>14</dd></div>
<div><dt>Students / yr</dt><dd>430</dd></div>
</dl>
</div>
<figure class="plan" aria-labelledby="plan-cap">
<svg id="planSvg" viewBox="0 0 420 300" role="img" aria-label="Cut plan drawing of a workbench with dimension lines">
<g class="draw" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- top -->
<path d="M40 92 L380 92 L380 112 L40 112 Z"/>
<path d="M40 92 L62 74 L402 74 L380 92"/>
<path d="M402 74 L402 94 L380 112"/>
<!-- legs -->
<path d="M64 112 L64 250"/><path d="M86 112 L86 250"/><path d="M64 250 L86 250"/>
<path d="M336 112 L336 250"/><path d="M358 112 L358 250"/><path d="M336 250 L358 250"/>
<!-- stretcher -->
<path d="M86 196 L336 196"/><path d="M86 212 L336 212"/>
<!-- vise -->
<path d="M40 118 L40 152 L104 152 L104 118"/>
<circle cx="72" cy="164" r="9"/>
<path d="M72 152 L72 155"/>
</g>
<g class="dim" fill="none" stroke="currentColor" stroke-width="1.2">
<path d="M40 62 L402 62"/><path d="M40 56 L40 68"/><path d="M402 56 L402 68"/>
<path d="M46 62 L54 58 M46 62 L54 66"/><path d="M396 62 L388 58 M396 62 L388 66"/>
<path d="M24 92 L24 250"/><path d="M18 92 L30 92"/><path d="M18 250 L30 250"/>
<path d="M24 98 L20 106 M24 98 L28 106"/><path d="M24 244 L20 236 M24 244 L28 236"/>
</g>
<text class="dimtext" x="221" y="52" text-anchor="middle">72"</text>
<text class="dimtext" x="14" y="176" text-anchor="middle" transform="rotate(-90 14 176)">34"</text>
<text class="dimtext" x="392" y="272" text-anchor="end">PLAN 04-A · REV 3</text>
</svg>
<figcaption id="plan-cap">Roubo-style bench · 8/4 hard maple top · drawbored M&T</figcaption>
</figure>
</div>
</section>
<!-- PROJECTS -->
<section id="projects" class="band">
<div class="wrap">
<header class="sec-head">
<h2>Four plans to start with</h2>
<p>Every plan ships as a dimensioned PDF, a cut list, and a hardware order sheet.</p>
</header>
<div class="cards" id="projectCards">
<article class="card">
<div class="iso">
<svg viewBox="0 0 120 100" aria-hidden="true">
<g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round">
<path d="M16 40 L60 20 L104 40 L60 60 Z" fill="rgba(169,127,79,.22)"/>
<path d="M16 40 L16 50 L60 70 L60 60"/><path d="M104 40 L104 50 L60 70"/>
<path d="M26 55 L26 86"/><path d="M94 55 L94 86"/><path d="M60 70 L60 92"/>
</g>
</svg>
</div>
<h3>The Alder Bench</h3>
<p>Laminated top, leg vise, dog holes on 4" centers.</p>
<div class="chips">
<span class="badge lvl-3">Advanced</span>
<span class="chip mono">28.5 bd ft</span>
<span class="chip mono">~24 h</span>
</div>
</article>
<article class="card">
<div class="iso">
<svg viewBox="0 0 120 100" aria-hidden="true">
<g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round">
<path d="M14 52 L60 30 L106 52 L60 74 Z" fill="rgba(169,127,79,.22)"/>
<path d="M14 52 L14 62 L60 84 L60 74"/><path d="M106 52 L106 62 L60 84"/>
<path d="M34 48 L80 70" opacity=".5"/><path d="M46 42 L92 64" opacity=".5"/>
</g>
</svg>
</div>
<h3>End-Grain Board</h3>
<p>Checkerboard glue-up in maple and walnut, juice groove.</p>
<div class="chips">
<span class="badge lvl-1">Beginner</span>
<span class="chip mono">4.0 bd ft</span>
<span class="chip mono">~6 h</span>
</div>
</article>
<article class="card">
<div class="iso">
<svg viewBox="0 0 120 100" aria-hidden="true">
<g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round">
<path d="M18 44 L64 22 L106 42 L60 64 Z" fill="rgba(169,127,79,.22)"/>
<path d="M18 44 L18 56 L60 76 L60 64"/><path d="M106 42 L106 54 L60 76"/>
<path d="M22 58 L22 74" stroke-dasharray="3 4"/><path d="M100 56 L100 72" stroke-dasharray="3 4"/>
</g>
</svg>
</div>
<h3>Floating Shelf</h3>
<p>Hidden steel rod cleat, 36" span, no visible hardware.</p>
<div class="chips">
<span class="badge lvl-2">Intermediate</span>
<span class="chip mono">6.5 bd ft</span>
<span class="chip mono">~9 h</span>
</div>
</article>
<article class="card">
<div class="iso">
<svg viewBox="0 0 120 100" aria-hidden="true">
<g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round">
<path d="M24 38 L60 20 L96 38 L60 56 Z" fill="rgba(169,127,79,.22)"/>
<path d="M32 44 L52 92"/><path d="M88 44 L68 92"/>
<path d="M42 68 L78 68"/>
</g>
</svg>
</div>
<h3>Camp Stool</h3>
<p>Steam-bent ash legs, canvas sling, folds flat to 2".</p>
<div class="chips">
<span class="badge lvl-2">Intermediate</span>
<span class="chip mono">3.2 bd ft</span>
<span class="chip mono">~7 h</span>
</div>
</article>
</div>
</div>
</section>
<!-- CALC + TOOLS -->
<section id="calc" class="band alt">
<div class="wrap split">
<form class="calc" id="calcForm" novalidate>
<h2>Cut-list calculator</h2>
<p class="sub">Rough-sawn stock, 8/4 thickness. Board feet = (T × W × L) ÷ 144.</p>
<div class="field">
<label for="w">Board width <span class="mono">in</span></label>
<input id="w" name="w" type="number" min="1" max="24" step="0.25" value="7.5" inputmode="decimal">
</div>
<div class="field">
<label for="l">Board length <span class="mono">in</span></label>
<input id="l" name="l" type="number" min="6" max="192" step="1" value="72" inputmode="decimal">
</div>
<div class="field">
<label for="q">Quantity</label>
<input id="q" name="q" type="number" min="1" max="60" step="1" value="4" inputmode="numeric">
</div>
<div class="field">
<label for="thick">Thickness</label>
<select id="thick">
<option value="1">4/4 — 1"</option>
<option value="1.5">6/4 — 1½"</option>
<option value="2" selected>8/4 — 2"</option>
<option value="3">12/4 — 3"</option>
</select>
</div>
<div class="readout" aria-live="polite">
<div><span class="k">Board feet</span><strong class="mono" id="outBf">—</strong></div>
<div><span class="k">Species</span><strong id="outSpecies">Walnut</strong></div>
<div><span class="k">Est. cost</span><strong class="mono accent" id="outCost">—</strong></div>
</div>
<button class="btn" type="submit">Add to cut list</button>
</form>
<div class="tools">
<h2>Tools of the shop</h2>
<p class="sub">Hover or focus a tool to read the shop notes.</p>
<ul class="tool-strip">
<li tabindex="0" data-note="Sawstop ICS · 3 hp · 52" fence. Blade change Tuesdays.">
<svg viewBox="0 0 64 48" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 34 H58"/><path d="M6 34 V42 H58 V34"/><circle cx="32" cy="24" r="13"/><path d="M32 11 V24 L43 30"/><path d="M19 34 A13 13 0 0 1 45 34"/></g></svg>
<span>Table saw</span>
</li>
<li tabindex="0" data-note="Set of six bevel-edge chisels, 1/8"–1". Strop before every use.">
<svg viewBox="0 0 64 48" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 30 L34 30 L44 24 L56 24 L56 32 L44 32 L34 38 L10 38 Z"/><path d="M34 30 V38"/><path d="M14 30 V38" opacity=".5"/></g></svg>
<span>Chisels</span>
</li>
<li tabindex="0" data-note="1/2" shank router in a lift table. Feed right to left, always.">
<svg viewBox="0 0 64 48" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M20 10 H44 V30 H20 Z"/><path d="M26 30 V38"/><path d="M38 30 V38"/><path d="M12 38 H52"/><path d="M32 10 V4"/></g></svg>
<span>Router</span>
</li>
<li tabindex="0" data-note="Twenty-four parallel clamps, 24"–50". Return them to the rack.">
<svg viewBox="0 0 64 48" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 12 H56"/><path d="M8 12 V30"/><path d="M40 12 V30"/><path d="M8 30 H18"/><path d="M40 30 H50"/><path d="M50 30 V38"/></g></svg>
<span>Clamps</span>
</li>
</ul>
<p class="tool-note mono" id="toolNote">Select a tool for its shop note.</p>
</div>
</div>
</section>
<!-- SPECIES -->
<section id="species" class="band">
<div class="wrap">
<header class="sec-head">
<h2>Pick your species</h2>
<p>Prices are per board foot, surfaced two sides, as of this month's mill run.</p>
</header>
<div class="species">
<div class="swatches" role="radiogroup" aria-label="Wood species">
<button class="sw" role="radio" aria-checked="false" data-species="pine"
data-label="Eastern White Pine" data-hard="380" data-price="4.20" data-tone="#e6cfa4" data-tone2="#c9ac7c"
data-desc="Soft, forgiving, cheap to practice on. Dents if you look at it wrong.">Pine</button>
<button class="sw" role="radio" aria-checked="false" data-species="oak"
data-label="Red Oak" data-hard="1290" data-price="7.85" data-tone="#d9b184" data-tone2="#b5854f"
data-desc="Open pore, loud grain, takes stain unevenly. Very hard wearing.">Oak</button>
<button class="sw" role="radio" aria-checked="true" data-species="walnut"
data-label="Black Walnut" data-hard="1010" data-price="14.60" data-tone="#8a6244" data-tone2="#4f3524"
data-desc="Chocolate heartwood, cuts like butter, finishes with just oil.">Walnut</button>
<button class="sw" role="radio" aria-checked="false" data-species="maple"
data-label="Hard Maple" data-hard="1450" data-price="9.30" data-tone="#f0dfbd" data-tone2="#d3ba8d"
data-desc="Tight and pale. Burns on a dull blade — keep the feed moving.">Maple</button>
</div>
<div class="preview" id="preview">
<div class="grain" id="grain" aria-hidden="true"></div>
<div class="preview-body">
<h3 id="spName">Black Walnut</h3>
<p id="spDesc">Chocolate heartwood, cuts like butter, finishes with just oil.</p>
<dl class="spstats">
<div><dt>Janka hardness</dt><dd class="mono" id="spHard">1010 lbf</dd></div>
<div><dt>Per board foot</dt><dd class="mono accent" id="spPrice">$14.60</dd></div>
</dl>
<div class="meter" role="img" aria-label="Relative hardness">
<span id="hardBar"></span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CLASSES -->
<section id="classes" class="band alt">
<div class="wrap">
<header class="sec-head">
<h2>Weekend workshops</h2>
<p>Small groups, shared benches, all consumables included.</p>
</header>
<div class="classes">
<article class="klass" data-seats="3" data-cap="10">
<span class="tag mono">SAT · 09:00</span>
<h3>Sharpening & Hand Tools</h3>
<p>Water stones, plane setup, and a chisel you take home tuned.</p>
<div class="seats"><div class="bar"><span></span></div><small class="mono"></small></div>
<button class="btn btn-ghost btn-sm book">Hold a seat</button>
</article>
<article class="klass" data-seats="6" data-cap="8">
<span class="tag mono">SUN · 10:30</span>
<h3>Dovetails in a Day</h3>
<p>Through-dovetails by hand, from layout to a finished small box.</p>
<div class="seats"><div class="bar"><span></span></div><small class="mono"></small></div>
<button class="btn btn-ghost btn-sm book">Hold a seat</button>
</article>
<article class="klass" data-seats="1" data-cap="12">
<span class="tag mono">SAT · 14:00</span>
<h3>Finishing Without Fear</h3>
<p>Oil, shellac and wax on twelve sample boards you compare side by side.</p>
<div class="seats"><div class="bar"><span></span></div><small class="mono"></small></div>
<button class="btn btn-ghost btn-sm book">Hold a seat</button>
</article>
</div>
</div>
</section>
<!-- TESTIMONIAL -->
<section class="band quote-band">
<div class="wrap">
<blockquote>
<p>“I came in able to cut a straight line and not much else. Six weekends later my
daughter eats breakfast at a table I made. Marisol never once made me feel slow.”</p>
<footer><strong>Dev Okonjo</strong><span class="mono"> · bench 6, spring cohort</span></footer>
</blockquote>
</div>
</section>
</main>
<footer class="foot">
<div class="wrap foot-in">
<div>
<h2>The Offcut</h2>
<p>One letter a month: a plan, a jig, and whatever came off the mill.</p>
<form class="news" id="newsForm" novalidate>
<label class="sr" for="email">Email address</label>
<input id="email" type="email" placeholder="[email protected]" autocomplete="email" required>
<button class="btn" type="submit">Subscribe</button>
</form>
<p class="err" id="newsErr" role="alert"></p>
</div>
<div class="foot-meta">
<p class="mono">GRAINHOUSE WOODWORKS<br>4 Alder St · Unit B<br>Tue–Sun 08:00–20:00</p>
<p class="fine">Fictional shop. Illustrative UI only.</p>
</div>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Woodworking Shop Landing
Grainhouse Woodworks is a fictional shop-and-school landing page set on a kraft-paper background of layered gradients. The hero pairs an italic serif headline and dual CTAs with a framed technical drawing: a Roubo-style bench in orthographic view, complete with burnt-orange dimension lines, arrowheads and mono callouts, that inks itself in when it scrolls into view.
Below it, four project cards each carry an isometric SVG piece on a hatched kraft plate, a colour-coded skill badge, and mono chips for board feet and build time. The cut-list calculator takes width, length, quantity and nominal thickness and recomputes board feet and estimated cost on every keystroke — priced against whichever species is currently selected, so the two sections stay in sync. The species swatches are a proper ARIA radiogroup with arrow-key navigation; picking one recolours the CSS wood-grain preview panel and animates the Janka hardness meter.
The tools strip reveals shop notes on hover, focus or click, the workshop cards decrement their
seats-left meters when you hold a seat (turning burnt-orange under two seats and locking to a
waitlist at zero), and the footer newsletter validates inline. Every action confirms through one
shared toast() helper.
How this look is built
The paper and the wood are both pure CSS: stacked repeating-linear-gradient layers at slightly
off-axis angles (92deg, 3deg, 97deg) fake fibre and grain far more cheaply than any texture image,
and the grain panel swaps only two custom properties — --t and --t2 — so a species change is a
single background-color transition instead of a repaint of four gradient stops. The hero drawing
uses the classic stroke-dasharray/stroke-dashoffset trick, but the path lengths are measured at
runtime with getTotalLength() and written back as a --len custom property, so the CSS keyframe
stays generic and every path finishes at the same time regardless of length; an IntersectionObserver
gates the animation until it is visible rather than firing it on load. Watch two things: only
stroke-dashoffset, transform, opacity and width are animated (all compositor- or
paint-cheap), and prefers-reduced-motion both snaps the drawing to its finished state and swaps
smooth anchor scrolling for instant jumps.
Illustrative UI only — always follow real safety guidance when working with tools, electronics, or repairs.