DIY — Crafts & Handmade Landing
A warm, tactile landing page for a fictional crafts community and supply shop, built with plain HTML, CSS and vanilla JavaScript. It pairs a blush hero with floating hand-drawn SVG doodles, filterable craft category tiles, tutorial cards with torn-paper illustration edges and yarn-ball difficulty ratings, a live supplies kit builder with running total and free-shipping threshold, a seasonal makealong band with an animated SVG progress ring, a keyboard-accessible community gallery lightbox, and a newsletter form with inline validation.
MCP
Code
:root{
--cream:#fdf6f1;
--cream-2:#f7ece4;
--blush:#fbe6df;
--terra:#d9755f;
--terra-d:#bc5b47;
--terra-soft:#f6ded7;
--sage:#8aa07a;
--sage-soft:#e6ede1;
--mustard:#e0a92e;
--mustard-soft:#faedcd;
--ink:#4a3a33;
--ink-2:#6b5750;
--muted:#9a847b;
--white:#fffdfb;
--line:rgba(74,58,51,.14);
--line-2:rgba(74,58,51,.24);
--ok:#5f8f63;
--danger:#c8543f;
--r-sm:8px; --r-md:16px; --r-lg:26px; --r-xl:34px;
--sh-1:0 2px 6px rgba(74,58,51,.06), 0 10px 24px rgba(74,58,51,.07);
--sh-2:0 6px 14px rgba(74,58,51,.09), 0 22px 48px rgba(74,58,51,.11);
--stitch: repeating-linear-gradient(90deg, var(--line-2) 0 7px, transparent 7px 14px);
}
*,*::before,*::after{box-sizing:border-box}
/* Keeps [hidden] winning over class rules that set a display value. */
[hidden]{display:none!important}
html{scroll-behavior:smooth}
body{
margin:0;
font-family:Inter,system-ui,sans-serif;
font-size:16px; line-height:1.5;
color:var(--ink);
background:var(--cream);
-webkit-font-smoothing:antialiased;
}
h1,h2,h3,p,ul,figure{margin:0}
ul{padding:0;list-style:none}
img,svg{max-width:100%}
a{color:inherit;text-decoration:none}
button,input{font:inherit;color:inherit}
.mono{font-family:"JetBrains Mono",ui-monospace,monospace}
.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:10px 16px;background:var(--ink);color:var(--cream);border-radius:var(--r-sm);z-index:99}
:focus-visible{outline:3px solid var(--terra);outline-offset:3px;border-radius:6px}
.wrap{width:min(1160px,100% - 2.5rem);margin-inline:auto}
/* ---------- buttons ---------- */
.btn{
display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
padding:.78rem 1.35rem;border:0;border-radius:999px;cursor:pointer;
background:var(--terra);color:#fff;font-weight:650;letter-spacing:.01em;
box-shadow:0 3px 0 var(--terra-d), var(--sh-1);
transition:transform .16s ease, box-shadow .16s ease, background .16s ease;
}
.btn:hover{background:var(--terra-d);transform:translateY(-2px);box-shadow:0 5px 0 #a44b39, var(--sh-2)}
.btn:active{transform:translateY(1px);box-shadow:0 1px 0 var(--terra-d)}
.btn-sm{padding:.5rem 1rem;font-size:.9rem}
.btn-ghost{background:transparent;color:var(--ink);box-shadow:inset 0 0 0 2px var(--line-2)}
.btn-ghost:hover{background:var(--white);color:var(--ink);box-shadow:inset 0 0 0 2px var(--terra), var(--sh-1)}
.btn-ghost[aria-pressed="true"]{background:var(--terra-soft);box-shadow:inset 0 0 0 2px var(--terra)}
.chip{display:inline-block;padding:.24rem .7rem;border-radius:999px;font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;font-family:"JetBrains Mono",monospace}
.chip-mustard{background:var(--mustard-soft);color:#8a6410}
/* ---------- topbar ---------- */
.topbar{position:sticky;top:0;z-index:40;background:rgba(253,246,241,.86);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.topbar-in{display:flex;align-items:center;gap:1.25rem;padding:.8rem 0}
.brand{display:flex;align-items:center;gap:.6rem;font-weight:800;font-size:1.05rem}
.brand-mark{width:36px;height:36px;color:var(--terra);display:grid;place-items:center;background:var(--terra-soft);border-radius:12px}
.brand-txt em{font-style:normal;color:var(--terra)}
.nav{display:flex;gap:1.4rem;margin-left:auto;font-weight:550;font-size:.95rem;color:var(--ink-2)}
.nav a{padding:.3rem 0;border-bottom:2px solid transparent;transition:color .15s,border-color .15s}
.nav a:hover{color:var(--terra);border-color:var(--terra)}
/* ---------- hero ---------- */
.hero{position:relative;background:
radial-gradient(60% 70% at 82% 18%, rgba(224,169,46,.16), transparent 60%),
radial-gradient(50% 60% at 12% 82%, rgba(138,160,122,.18), transparent 62%),
linear-gradient(180deg,var(--blush),var(--cream-2));
padding:5.5rem 0 7rem;overflow:hidden}
.hero-in{position:relative;z-index:2;max-width:660px}
.eyebrow{font-family:"JetBrains Mono",monospace;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--terra-d);margin-bottom:1rem}
h1{font-size:clamp(2.35rem,6vw,4.1rem);line-height:1.05;letter-spacing:-.03em;font-weight:800}
.hl{position:relative;color:var(--terra)}
.hl::after{content:"";position:absolute;left:-2px;right:-2px;bottom:.06em;height:.3em;background:var(--mustard-soft);border-radius:50% 50% 40% 60%/60% 40% 60% 40%;z-index:-1}
.lede{margin-top:1.15rem;font-size:1.1rem;color:var(--ink-2);max-width:48ch}
.hero-cta{display:flex;flex-wrap:wrap;gap:.8rem;margin-top:1.8rem}
.hero-stats{display:flex;gap:2.2rem;margin-top:2.6rem;padding-top:1.6rem;border-top:2px dashed var(--line-2)}
.hero-stats b{display:block;font-size:1.6rem;font-weight:800;font-family:"JetBrains Mono",monospace;color:var(--terra)}
.hero-stats span{font-size:.85rem;color:var(--muted)}
.doodles{position:absolute;inset:0;z-index:1;pointer-events:none;color:var(--terra)}
.dd{position:absolute;width:96px;opacity:.55;animation:float 7s ease-in-out infinite}
.dd-1{top:12%;right:8%;color:var(--terra)}
.dd-2{top:44%;right:22%;width:120px;color:var(--sage);animation-delay:-1.6s}
.dd-3{bottom:16%;right:11%;color:var(--mustard);animation-delay:-3.2s}
.dd-4{top:20%;right:33%;width:78px;color:var(--ink-2);opacity:.35;animation-delay:-4.6s}
@keyframes float{0%,100%{transform:translateY(0) rotate(-3deg)}50%{transform:translateY(-16px) rotate(4deg)}}
.torn{position:absolute;left:0;right:0;height:22px;background:var(--cream);
-webkit-mask:radial-gradient(12px 22px at 12px -6px, transparent 98%, #000 100%) repeat-x 0 0/24px 22px;
mask:radial-gradient(12px 22px at 12px -6px, transparent 98%, #000 100%) repeat-x 0 0/24px 22px}
.torn-bottom{bottom:0}
.torn-top{top:0;transform:scaleY(-1);background:var(--cream)}
/* ---------- sections ---------- */
.section{padding:4.5rem 0}
.section-alt{background:var(--cream-2)}
.sec-h{font-size:clamp(1.6rem,3.2vw,2.25rem);letter-spacing:-.02em;font-weight:800}
.sec-p{color:var(--muted);margin-top:.35rem}
.sec-row{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:1.6rem}
/* ---------- categories ---------- */
.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:.9rem;margin-top:1.6rem}
.cat{
display:flex;flex-direction:column;align-items:center;gap:.5rem;
padding:1.15rem .7rem;border:2px solid var(--line);border-radius:var(--r-lg);
background:var(--white);cursor:pointer;position:relative;
transition:transform .16s ease,border-color .16s ease,box-shadow .16s ease,background .16s ease;
}
.cat:hover{transform:translateY(-4px) rotate(-1deg);border-color:var(--terra);box-shadow:var(--sh-1)}
.cat-ic{width:44px;height:44px;color:var(--sage);transition:color .16s}
.cat-t{font-weight:650;font-size:.95rem}
.cat-n{font-family:"JetBrains Mono",monospace;font-size:.72rem;color:var(--muted)}
.cat.is-on{background:var(--terra-soft);border-color:var(--terra);border-style:dashed;box-shadow:var(--sh-1)}
.cat.is-on .cat-ic{color:var(--terra-d)}
.cat.is-on .cat-n{color:var(--terra-d)}
/* ---------- tutorial grid ---------- */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(272px,1fr));gap:1.4rem}
.card-t{
background:var(--white);border-radius:var(--r-lg);overflow:hidden;
box-shadow:var(--sh-1);border:1px solid var(--line);
display:flex;flex-direction:column;position:relative;
transition:transform .2s ease, box-shadow .2s ease;
animation:pop .32s ease both;
}
.card-t:hover{transform:translateY(-6px);box-shadow:var(--sh-2)}
@keyframes pop{from{opacity:0;transform:translateY(14px) scale(.98)}to{opacity:1;transform:none}}
.art{position:relative;height:158px}
.art::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:16px;background:var(--white);
-webkit-mask:radial-gradient(10px 16px at 10px -5px, transparent 98%, #000 100%) repeat-x 0 0/20px 16px;
mask:radial-gradient(10px 16px at 10px -5px, transparent 98%, #000 100%) repeat-x 0 0/20px 16px}
.art svg{position:absolute;inset:0;width:100%;height:100%}
.t-body{padding:1rem 1.1rem 1.2rem;display:flex;flex-direction:column;gap:.6rem;flex:1}
.t-title{font-size:1.06rem;font-weight:700;letter-spacing:-.01em;line-height:1.25}
.t-meta{display:flex;align-items:center;gap:.55rem;font-size:.83rem;color:var(--ink-2)}
.avatar{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;font-size:.66rem;font-weight:700;font-family:"JetBrains Mono",monospace;color:#fff;background:var(--sage)}
.badge{margin-left:auto;font-family:"JetBrains Mono",monospace;font-size:.72rem;background:var(--sage-soft);color:#4d6041;padding:.2rem .55rem;border-radius:999px}
.t-foot{display:flex;align-items:center;gap:.5rem;margin-top:auto;padding-top:.7rem;border-top:2px dotted var(--line-2)}
.yarn{display:flex;gap:.22rem;color:var(--mustard)}
.yarn svg{width:16px;height:16px}
.yarn .off{color:var(--line-2)}
.diff{font-size:.78rem;color:var(--muted);margin-left:.15rem}
.save{margin-left:auto;width:36px;height:36px;border-radius:50%;border:2px solid var(--line-2);background:transparent;display:grid;place-items:center;cursor:pointer;color:var(--muted);transition:.16s}
.save:hover{border-color:var(--terra);color:var(--terra);transform:scale(1.08)}
.save[aria-pressed="true"]{background:var(--terra);border-color:var(--terra);color:#fff}
.save svg{width:17px;height:17px}
.empty{text-align:center;color:var(--muted);padding:3rem 0}
/* ---------- two column ---------- */
.two{display:grid;grid-template-columns:1fr 1fr;gap:1.6rem;align-items:start}
.card{background:var(--white);border:1px solid var(--line);border-radius:var(--r-xl);padding:1.7rem;box-shadow:var(--sh-1);position:relative}
.card::before{content:"";position:absolute;inset:9px;border-radius:calc(var(--r-xl) - 9px);border:2px dashed rgba(74,58,51,.13);pointer-events:none}
.card-h{font-size:1.4rem;font-weight:800;letter-spacing:-.02em}
.card-p{color:var(--ink-2);margin-top:.4rem;font-size:.95rem}
/* kit */
.kit-list{margin-top:1.2rem;display:grid;gap:.15rem}
.kit-list label{display:flex;align-items:center;gap:.75rem;padding:.6rem .7rem;border-radius:var(--r-md);cursor:pointer;transition:background .14s}
.kit-list label:hover{background:var(--cream-2)}
.kit-list input{appearance:none;width:22px;height:22px;flex:none;border:2px solid var(--line-2);border-radius:7px;display:grid;place-items:center;cursor:pointer;transition:.15s}
.kit-list input:checked{background:var(--sage);border-color:var(--sage)}
.kit-list input:checked::after{content:"";width:11px;height:6px;border:2.5px solid #fff;border-top:0;border-right:0;transform:rotate(-45deg) translateY(-2px)}
.kl-n{font-size:.94rem}
.kl-n em{font-style:normal;color:var(--muted);font-size:.85rem}
.kl-p{margin-left:auto;font-family:"JetBrains Mono",monospace;font-size:.88rem;color:var(--ink-2)}
.basket{display:flex;align-items:center;gap:1rem;margin-top:1.3rem;padding:1rem;border-radius:var(--r-lg);background:var(--terra-soft)}
.basket-ic{position:relative;width:46px;height:40px;color:var(--terra-d);flex:none}
.basket-badge{position:absolute;top:-8px;right:-10px;min-width:22px;height:22px;border-radius:999px;background:var(--terra);color:#fff;font-size:.72rem;font-weight:700;display:grid;place-items:center;font-family:"JetBrains Mono",monospace}
.basket-badge.bump{animation:bump .32s ease}
@keyframes bump{50%{transform:scale(1.35)}}
.basket-tot{display:flex;flex-direction:column;line-height:1.2}
.basket-tot span{font-size:.78rem;color:var(--ink-2)}
.basket-tot b{font-size:1.45rem;font-family:"JetBrains Mono",monospace;color:var(--terra-d)}
.basket-tot small{font-size:.72rem;color:var(--muted)}
#kit-add{margin-left:auto}
/* makealong */
.ring-row{display:flex;gap:1.4rem;align-items:center;margin-top:1.4rem;flex-wrap:wrap}
.ring{position:relative;width:132px;height:132px;flex:none}
.ring svg{transform:rotate(-90deg)}
.ring-bg{fill:none;stroke:var(--cream-2);stroke-width:12}
.ring-fg{fill:none;stroke:var(--mustard);stroke-width:12;stroke-linecap:round;stroke-dasharray:314;stroke-dashoffset:314;transition:stroke-dashoffset 1.1s cubic-bezier(.3,.9,.3,1)}
.ring-txt{position:absolute;inset:0;display:grid;place-content:center;text-align:center}
.ring-txt b{font-size:1.9rem;font-family:"JetBrains Mono",monospace;font-weight:700}
.ring-txt b span{font-size:1rem;color:var(--muted)}
.ring-txt small{font-size:.72rem;color:var(--muted);letter-spacing:.1em;text-transform:uppercase}
.weeks{display:grid;gap:.3rem;font-size:.9rem;flex:1;min-width:190px}
.weeks li{padding-left:1.5rem;position:relative;color:var(--muted)}
.weeks li::before{content:"";position:absolute;left:0;top:.45em;width:10px;height:10px;border-radius:50%;border:2px solid var(--line-2)}
.weeks .done{color:var(--ink-2)}
.weeks .done::before{background:var(--sage);border-color:var(--sage)}
.weeks .now{color:var(--ink);font-weight:700}
.weeks .now::before{background:var(--mustard);border-color:var(--mustard);box-shadow:0 0 0 4px var(--mustard-soft)}
.makealong-foot{display:flex;align-items:center;gap:.8rem;margin-top:1.4rem;padding-top:1.1rem;border-top:2px dotted var(--line-2);flex-wrap:wrap}
.faces{display:flex}
.faces span{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-size:.66rem;font-weight:700;font-family:"JetBrains Mono",monospace;color:#fff;background:var(--terra);border:2px solid var(--white);margin-left:-8px}
.faces span:first-child{margin-left:0;background:var(--sage)}
.faces span:nth-child(3){background:var(--mustard);color:var(--ink)}
.faces span:nth-child(4){background:var(--ink-2)}
.makealong-foot p{font-size:.88rem;color:var(--ink-2)}
#join{margin-left:auto}
/* ---------- gallery ---------- */
.gal{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem;margin-top:1.6rem}
.gal-item{position:relative;border:0;padding:0;border-radius:var(--r-lg);overflow:hidden;cursor:pointer;aspect-ratio:1;box-shadow:var(--sh-1);transition:transform .18s ease, box-shadow .18s ease}
.gal-item:hover{transform:scale(1.03) rotate(-1deg);box-shadow:var(--sh-2)}
.gal-item svg{width:100%;height:100%;display:block}
.gal-cap{position:absolute;left:0;right:0;bottom:0;padding:.7rem;font-size:.78rem;font-weight:600;text-align:left;color:#fff;background:linear-gradient(transparent,rgba(74,58,51,.78))}
/* lightbox */
.lb{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1.2rem}
.lb-back{position:absolute;inset:0;background:rgba(74,58,51,.68);backdrop-filter:blur(4px);animation:fade .2s ease}
@keyframes fade{from{opacity:0}to{opacity:1}}
.lb-box{position:relative;background:var(--white);border-radius:var(--r-xl);padding:1rem;max-width:min(520px,92vw);box-shadow:var(--sh-2);animation:pop .24s ease both}
.lb-art{border-radius:var(--r-lg);overflow:hidden;aspect-ratio:1}
.lb-art svg{width:100%;height:100%;display:block}
.lb-box figcaption{padding:.85rem .4rem .2rem;font-weight:650}
.lb-x{position:absolute;top:-14px;right:-14px;width:40px;height:40px;border-radius:50%;border:0;background:var(--ink);color:#fff;font-size:1.4rem;cursor:pointer;line-height:1}
.lb-nav{position:absolute;top:45%;width:42px;height:42px;border-radius:50%;border:0;background:var(--white);color:var(--ink);font-size:1.6rem;line-height:1;cursor:pointer;box-shadow:var(--sh-1)}
.lb-prev{left:-20px}.lb-next{right:-20px}
.lb-nav:hover{background:var(--terra);color:#fff}
/* ---------- footer ---------- */
.foot{position:relative;background:var(--cream-2);padding:4.5rem 0 2rem;margin-top:1rem}
.foot-in{display:grid;grid-template-columns:1fr 1fr;gap:2rem;align-items:center}
.nl-row{display:flex;gap:.6rem;flex-wrap:wrap}
.nl input{flex:1;min-width:200px;padding:.8rem 1.05rem;border-radius:999px;border:2px solid var(--line-2);background:var(--white);transition:border-color .15s}
.nl input:hover{border-color:var(--muted)}
.nl input.bad{border-color:var(--danger);background:#fdf0ed}
.nl input.good{border-color:var(--ok)}
.nl-msg{min-height:1.3em;margin-top:.55rem;font-size:.85rem;color:var(--muted)}
.nl-msg.bad{color:var(--danger)}
.nl-msg.good{color:var(--ok)}
.foot-legal{display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-top:3rem;padding-top:1.2rem;border-top:2px dashed var(--line-2);font-size:.8rem;color:var(--muted)}
/* ---------- toast ---------- */
.toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%,140%);z-index:90;
background:var(--ink);color:var(--cream);padding:.75rem 1.25rem;border-radius:999px;
font-size:.9rem;font-weight:550;box-shadow:var(--sh-2);transition:transform .3s cubic-bezier(.2,1.2,.4,1);pointer-events:none;max-width:88vw;text-align:center}
.toast.on{transform:translate(-50%,0)}
/* ---------- responsive ---------- */
@media (max-width:900px){
.two,.foot-in{grid-template-columns:1fr}
.nav{display:none}
}
@media (max-width:520px){
.wrap{width:min(1160px,100% - 1.5rem)}
.hero{padding:3.2rem 0 4.5rem}
.doodles{opacity:.5}
.dd{width:64px}.dd-2{width:78px}.dd-4{display:none}
.hero-stats{gap:1.2rem}
.hero-stats b{font-size:1.25rem}
.section{padding:3rem 0}
.cats{grid-template-columns:repeat(2,1fr)}
.grid{grid-template-columns:1fr}
.gal{grid-template-columns:repeat(2,1fr)}
.card{padding:1.15rem}
.card::before{inset:6px}
.basket{flex-wrap:wrap}
#kit-add,#join{margin-left:0;width:100%}
.lb-prev{left:6px}.lb-next{right:6px}
.brand-txt{font-size:.95rem}
}
@media (prefers-reduced-motion:reduce){
html{scroll-behavior:auto}
*,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important}
}/* Little Loom Studio — crafts landing interactions (vanilla JS) */
(function () {
"use strict";
/* ---------- toast ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("on"); }, 2600);
}
/* ---------- data ---------- */
var TUTORIALS = [
{ id: "t1", cat: "knitting", title: "Ribbed Beanie in an Evening", maker: "Nora Reyes", ini: "NR", time: "2 h", diff: 1, a: "#f6ded7", b: "#e8bfae" },
{ id: "t2", cat: "knitting", title: "Chunky Blanket, No Needles", maker: "Tomás Ilic", ini: "TI", time: "3 h", diff: 2, a: "#e6ede1", b: "#bccdb0" },
{ id: "t3", cat: "knitting", title: "Colourwork Mittens Primer", maker: "Aiko Lund", ini: "AL", time: "5 h", diff: 3, a: "#faedcd", b: "#e6c88a" },
{ id: "t4", cat: "embroidery", title: "Satin Stitch Wildflowers", maker: "Marta Kell", ini: "MK", time: "90 min", diff: 2, a: "#f6ded7", b: "#d9a898" },
{ id: "t5", cat: "embroidery", title: "Mending Jeans, Visibly", maker: "Josie Brandt", ini: "JB", time: "45 min", diff: 1, a: "#e2e8ee", b: "#adbccc" },
{ id: "t6", cat: "paper", title: "Deckle-Edge Recycled Sheets", maker: "Peter Vance", ini: "PV", time: "2 h", diff: 2, a: "#f4ece2", b: "#dbc7b0" },
{ id: "t7", cat: "paper", title: "Six-Fold Pop-Up Cards", maker: "Suri Nayar", ini: "SN", time: "60 min", diff: 1, a: "#faedcd", b: "#ecd39b" },
{ id: "t8", cat: "candles", title: "Beeswax Tapers, Hand Dipped", maker: "Ola Fenn", ini: "OF", time: "2.5 h", diff: 2, a: "#faedcd", b: "#e0a92e" },
{ id: "t9", cat: "candles", title: "Soy Tins with Cotton Wicks", maker: "Ola Fenn", ini: "OF", time: "70 min", diff: 1, a: "#f0e8e2", b: "#cfbcae" },
{ id: "t10", cat: "pottery", title: "Pinch Pots & Thumb Bowls", maker: "Devin Aoki", ini: "DA", time: "3 h", diff: 2, a: "#e9ddd4", b: "#c39b81" },
{ id: "t11", cat: "pottery", title: "Slab-Built Planter Trio", maker: "Devin Aoki", ini: "DA", time: "4 h", diff: 3, a: "#e6ede1", b: "#a7bb96" },
{ id: "t12", cat: "resin", title: "Pressed-Petal Coasters", maker: "Lena Crisp", ini: "LC", time: "24 h cure", diff: 3, a: "#f2e4ef", b: "#cfa8c6" }
];
var GALLERY = [
{ t: "Sage cardigan, first one!", by: "Nora R.", a: "#e6ede1", b: "#8aa07a" },
{ t: "Hoop of poppies", by: "Marta K.", a: "#f6ded7", b: "#d9755f" },
{ t: "Beeswax tapers, batch 4", by: "Ola F.", a: "#faedcd", b: "#e0a92e" },
{ t: "Speckled breakfast bowl", by: "Devin A.", a: "#efe5dc", b: "#b08e75" },
{ t: "Petal coasters set", by: "Lena C.", a: "#f2e4ef", b: "#b98bb0" },
{ t: "Pop-up birthday card", by: "Suri N.", a: "#e2e8ee", b: "#7f97ae" },
{ t: "Visible mend on denim", by: "Josie B.", a: "#dfe6ee", b: "#5b7188" },
{ t: "Loom weave, week 2", by: "Aiko L.", a: "#f7ece4", b: "#c9a68b" }
];
/* ---------- svg art (all CSS/SVG drawn, no images) ---------- */
var seedN = 0;
function art(a, b, seed) {
var s = seed == null ? seedN++ : seed;
var r = function (i) { return ((Math.sin((s + 1) * (i + 3) * 12.9898) * 43758.5453) % 1 + 1) % 1; };
var dots = "";
for (var i = 0; i < 9; i++) {
dots += '<circle cx="' + (14 + r(i) * 172).toFixed(1) + '" cy="' + (14 + r(i + 20) * 132).toFixed(1) +
'" r="' + (2 + r(i + 40) * 4).toFixed(1) + '" fill="' + b + '" opacity="' + (0.25 + r(i + 60) * 0.4).toFixed(2) + '"/>';
}
return '<svg viewBox="0 0 200 160" preserveAspectRatio="none" role="presentation">' +
'<rect width="200" height="160" fill="' + a + '"/>' +
'<path d="M0 118 Q34 96 68 116 T136 112 T200 122 V160 H0Z" fill="' + b + '" opacity=".45"/>' +
'<circle cx="' + (46 + r(2) * 100).toFixed(0) + '" cy="62" r="34" fill="' + b + '" opacity=".55"/>' +
'<path d="M20 66 Q60 24 104 60 T182 52" stroke="' + b + '" stroke-width="4" fill="none" stroke-linecap="round" opacity=".8"/>' +
dots + "</svg>";
}
function yarnIcon(on) {
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="' + (on ? "" : "off") +
'"><circle cx="12" cy="12" r="9"/><path d="M4 9c6 1 12 5 15 11M5 16c4-6 9-10 14-11M12 3c-3 5-4 12-2 18"/></svg>';
}
/* ---------- state ---------- */
var state = { cat: "all", savedOnly: false, saved: {} };
var grid = document.getElementById("grid");
var emptyEl = document.getElementById("empty");
var countEl = document.getElementById("tut-count");
function visible() {
return TUTORIALS.filter(function (t) {
if (state.cat !== "all" && t.cat !== state.cat) return false;
if (state.savedOnly && !state.saved[t.id]) return false;
return true;
});
}
function render() {
var list = visible();
grid.innerHTML = list.map(function (t, i) {
var yarns = "";
for (var k = 1; k <= 3; k++) yarns += yarnIcon(k <= t.diff);
var label = ["", "Beginner", "Comfortable", "Ambitious"][t.diff];
return '<article class="card-t" style="animation-delay:' + (i * 35) + 'ms">' +
'<div class="art">' + art(t.a, t.b, i + t.title.length) + "</div>" +
'<div class="t-body">' +
'<h3 class="t-title">' + t.title + "</h3>" +
'<div class="t-meta"><span class="avatar" aria-hidden="true">' + t.ini + "</span>" + t.maker +
'<span class="badge">' + t.time + "</span></div>" +
'<div class="t-foot"><span class="yarn" role="img" aria-label="Difficulty: ' + label + '">' + yarns + "</span>" +
'<span class="diff">' + label + "</span>" +
'<button class="save" data-save="' + t.id + '" aria-pressed="' + (state.saved[t.id] ? "true" : "false") +
'" aria-label="Save ' + t.title.replace(/&/g, "and") + '">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"><path d="M6 3h12v18l-6-5-6 5z"/></svg></button>' +
"</div></div></article>";
}).join("");
emptyEl.hidden = list.length > 0;
countEl.textContent = "Showing " + list.length + " tutorial" + (list.length === 1 ? "" : "s");
}
render();
/* saves */
grid.addEventListener("click", function (e) {
var btn = e.target.closest("[data-save]");
if (!btn) return;
var id = btn.getAttribute("data-save");
state.saved[id] = !state.saved[id];
btn.setAttribute("aria-pressed", state.saved[id] ? "true" : "false");
toast(state.saved[id] ? "Saved to your basket of ideas" : "Removed from saved");
if (state.savedOnly) render();
});
/* categories */
var cats = document.getElementById("cats");
cats.addEventListener("click", function (e) {
var btn = e.target.closest(".cat");
if (!btn) return;
Array.prototype.forEach.call(cats.querySelectorAll(".cat"), function (b) {
var on = b === btn;
b.classList.toggle("is-on", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
});
state.cat = btn.getAttribute("data-cat");
render();
});
var savedBtn = document.getElementById("saved-only");
savedBtn.addEventListener("click", function () {
state.savedOnly = !state.savedOnly;
savedBtn.setAttribute("aria-pressed", state.savedOnly ? "true" : "false");
savedBtn.textContent = state.savedOnly ? "Show all" : "Saved only";
render();
});
/* ---------- kit basket ---------- */
var kitList = document.getElementById("kit-list");
var totalEl = document.getElementById("kit-total");
var countKit = document.getElementById("kit-count");
var shipEl = document.getElementById("kit-ship");
function updateKit(bump) {
var boxes = kitList.querySelectorAll("input:checked");
var total = 0;
Array.prototype.forEach.call(boxes, function (b) { total += parseFloat(b.dataset.price); });
totalEl.textContent = "$" + total.toFixed(2);
countKit.textContent = String(boxes.length);
shipEl.textContent = total >= 40
? "Free shipping unlocked"
: "$" + (40 - total).toFixed(2) + " to free shipping";
if (bump) {
countKit.classList.remove("bump");
void countKit.offsetWidth;
countKit.classList.add("bump");
}
}
kitList.addEventListener("change", function () { updateKit(true); });
updateKit(false);
document.getElementById("kit-add").addEventListener("click", function () {
var n = kitList.querySelectorAll("input:checked").length;
toast(n ? n + " item" + (n === 1 ? "" : "s") + " added — " + totalEl.textContent : "Tick at least one supply first");
});
/* ---------- progress ring ---------- */
var ring = document.getElementById("ring-fg");
var C = 2 * Math.PI * 50;
ring.style.strokeDasharray = C;
ring.style.strokeDashoffset = C;
function fillRing() { ring.style.strokeDashoffset = C * (1 - 3 / 6); }
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(function (es) {
es.forEach(function (en) { if (en.isIntersecting) { fillRing(); io.disconnect(); } });
}, { threshold: 0.4 });
io.observe(document.getElementById("makealong"));
} else { fillRing(); }
var joinBtn = document.getElementById("join");
joinBtn.addEventListener("click", function () {
joinBtn.textContent = "You're in ✓";
joinBtn.disabled = true;
toast("Week 3 chapter sent to your inbox");
});
/* ---------- gallery + lightbox ---------- */
var gal = document.getElementById("gal");
gal.innerHTML = GALLERY.map(function (g, i) {
return '<button class="gal-item" data-i="' + i + '" aria-label="Expand: ' + g.t + '">' +
art(g.a, g.b, i * 7 + 3) + '<span class="gal-cap">' + g.t + " · " + g.by + "</span></button>";
}).join("");
var lb = document.getElementById("lb");
var lbArt = document.getElementById("lb-art");
var lbCap = document.getElementById("lb-cap");
var lastFocus = null;
var cur = 0;
function show(i) {
cur = (i + GALLERY.length) % GALLERY.length;
var g = GALLERY[cur];
lbArt.innerHTML = art(g.a, g.b, cur * 7 + 3);
lbCap.textContent = g.t + " — by " + g.by;
}
function open(i) {
lastFocus = document.activeElement;
show(i);
lb.hidden = false;
document.body.style.overflow = "hidden";
document.getElementById("lb-x").focus();
}
function close() {
lb.hidden = true;
document.body.style.overflow = "";
if (lastFocus) lastFocus.focus();
}
gal.addEventListener("click", function (e) {
var b = e.target.closest(".gal-item");
if (b) open(parseInt(b.dataset.i, 10));
});
lb.addEventListener("click", function (e) {
if (e.target.hasAttribute("data-close")) close();
});
document.getElementById("lb-x").addEventListener("click", close);
document.getElementById("lb-prev").addEventListener("click", function () { show(cur - 1); });
document.getElementById("lb-next").addEventListener("click", function () { show(cur + 1); });
document.addEventListener("keydown", function (e) {
if (lb.hidden) return;
if (e.key === "Escape") close();
if (e.key === "ArrowLeft") show(cur - 1);
if (e.key === "ArrowRight") show(cur + 1);
if (e.key === "Tab") {
var f = lb.querySelectorAll("button");
var first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
});
/* ---------- newsletter ---------- */
var form = document.getElementById("nl");
var mail = document.getElementById("nl-mail");
var msg = document.getElementById("nl-msg");
var RE = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i;
function validate(quiet) {
var v = mail.value.trim();
mail.classList.remove("bad", "good");
msg.classList.remove("bad", "good");
if (!v) { if (!quiet) { msg.textContent = "We need an address to post the letter."; msg.classList.add("bad"); mail.classList.add("bad"); } else { msg.textContent = ""; } return false; }
if (!RE.test(v)) { msg.textContent = "That doesn't look like an email yet."; msg.classList.add("bad"); mail.classList.add("bad"); return false; }
msg.textContent = "Looks good."; msg.classList.add("good"); mail.classList.add("good"); return true;
}
mail.addEventListener("input", function () { validate(true); });
mail.addEventListener("blur", function () { if (mail.value.trim()) validate(false); });
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!validate(false)) { mail.focus(); return; }
msg.textContent = "Welcome in — first letter lands Thursday.";
msg.classList.add("good");
form.reset();
mail.classList.remove("good");
toast("Subscribed to the Little Loom letter");
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Little Loom Studio — Crafts & Handmade</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="topbar">
<div class="wrap topbar-in">
<a class="brand" href="#main">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
<circle cx="20" cy="20" r="12" />
<path d="M9 15c7 3 15 3 22 0M9 25c7-3 15-3 22 0M20 8c-4 7-4 17 0 24M20 8c4 7 4 17 0 24" />
</svg>
</span>
<span class="brand-txt">Little Loom <em>Studio</em></span>
</a>
<nav class="nav" aria-label="Primary">
<a href="#tutorials">Tutorials</a>
<a href="#kit">Supplies</a>
<a href="#makealong">Makealong</a>
<a href="#gallery">Community</a>
</nav>
<a class="btn btn-sm" href="#tutorials">Browse tutorials</a>
</div>
</header>
<main id="main">
<!-- HERO -->
<section class="hero">
<div class="doodles" aria-hidden="true">
<svg class="dd dd-1" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<circle cx="16" cy="46" r="9" /><circle cx="40" cy="46" r="9" />
<path d="M22 40 52 12M34 40 52 20M52 12v10" />
</svg>
<svg class="dd dd-2" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<circle cx="30" cy="32" r="18" />
<path d="M14 24c12 2 24 10 30 22M18 42c8-12 20-19 30-20M30 14c-6 10-8 24-4 36" />
<path d="M48 44c6 3 10 7 12 12" />
</svg>
<svg class="dd dd-3" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<path d="M14 54 34 34M34 34c8-8 10-16 16-22 2 6-4 12-10 20l-6 2z" />
<path d="M28 40l-8 8" />
</svg>
<svg class="dd dd-4" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<path d="M50 8 22 46" /><ellipse cx="52" cy="7" rx="4" ry="6" transform="rotate(38 52 7)" />
<path d="M22 46l-6 10 10-6" />
</svg>
</div>
<div class="wrap hero-in">
<p class="eyebrow">Since 2019 · 14,300 makers</p>
<h1>Make something<br /><span class="hl">with your hands</span> today.</h1>
<p class="lede">Free step-by-step tutorials, curated supply kits and a slow makealong club — from a tiny studio in Grangewood.</p>
<div class="hero-cta">
<a class="btn" href="#tutorials">Browse tutorials</a>
<a class="btn btn-ghost" href="#makealong">Join the makealong</a>
</div>
<ul class="hero-stats">
<li><b>212</b><span>tutorials</span></li>
<li><b>6</b><span>craft paths</span></li>
<li><b>4.8</b><span>maker rating</span></li>
</ul>
</div>
<div class="torn torn-bottom" aria-hidden="true"></div>
</section>
<!-- CATEGORIES -->
<section class="section" id="categories" aria-labelledby="cat-h">
<div class="wrap">
<h2 id="cat-h" class="sec-h">Pick a craft</h2>
<p class="sec-p">Tap a tile to filter the tutorials below.</p>
<div class="cats" role="group" aria-label="Craft categories" id="cats">
<button class="cat is-on" data-cat="all" aria-pressed="true">
<span class="cat-ic" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"><circle cx="24" cy="24" r="15"/><path d="M9 20c10 3 20 3 30 0M9 28c10-3 20-3 30 0M24 9c-5 9-5 21 0 30M24 9c5 9 5 21 0 30"/></svg></span>
<span class="cat-t">All crafts</span><span class="cat-n">12</span>
</button>
<button class="cat" data-cat="knitting" aria-pressed="false">
<span class="cat-ic" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"><path d="M10 34 34 10M14 38 38 14"/><path d="M8 36c-2 2-2 5 0 6s5 0 6-2M10 12c-2-2-2 2 2 4"/><path d="M18 30c4 4 8 4 12 0"/></svg></span>
<span class="cat-t">Knitting</span><span class="cat-n">3</span>
</button>
<button class="cat" data-cat="embroidery" aria-pressed="false">
<span class="cat-ic" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"><path d="M38 10 16 32"/><path d="M38 10l2 6-6-2z"/><path d="M16 32c-6 2-8 6-8 8 4 0 8-2 10-8"/><path d="M12 20c4-4 10-4 14 0"/></svg></span>
<span class="cat-t">Embroidery</span><span class="cat-n">2</span>
</button>
<button class="cat" data-cat="paper" aria-pressed="false">
<span class="cat-ic" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linejoin="round" stroke-linecap="round"><path d="M12 8h16l8 8v24H12z"/><path d="M28 8v8h8"/><path d="M17 24h14M17 30h10"/></svg></span>
<span class="cat-t">Paper</span><span class="cat-n">2</span>
</button>
<button class="cat" data-cat="candles" aria-pressed="false">
<span class="cat-ic" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><rect x="16" y="20" width="16" height="20" rx="3"/><path d="M24 20v-4"/><path d="M24 8c4 4 4 8 0 8s-4-4 0-8z"/></svg></span>
<span class="cat-t">Candles</span><span class="cat-n">2</span>
</button>
<button class="cat" data-cat="pottery" aria-pressed="false">
<span class="cat-ic" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M16 14h16l-3 6c5 3 7 8 7 12 0 5-5 8-12 8s-12-3-12-8c0-4 2-9 7-12z"/><path d="M14 28h20"/></svg></span>
<span class="cat-t">Pottery</span><span class="cat-n">2</span>
</button>
<button class="cat" data-cat="resin" aria-pressed="false">
<span class="cat-ic" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M24 8c7 9 11 14 11 20a11 11 0 1 1-22 0c0-6 4-11 11-20z"/><path d="M19 28c0 4 2 6 5 6"/></svg></span>
<span class="cat-t">Resin</span><span class="cat-n">1</span>
</button>
</div>
</div>
</section>
<!-- TUTORIALS -->
<section class="section section-alt" id="tutorials" aria-labelledby="tut-h">
<div class="wrap">
<div class="sec-row">
<div>
<h2 id="tut-h" class="sec-h">Tutorials</h2>
<p class="sec-p" id="tut-count" aria-live="polite">Showing 12 tutorials</p>
</div>
<button class="btn btn-ghost btn-sm" id="saved-only" aria-pressed="false">Saved only</button>
</div>
<div class="grid" id="grid"><!-- injected --></div>
<p class="empty" id="empty" hidden>No tutorials in that craft yet — try another tile.</p>
</div>
</section>
<!-- KIT + MAKEALONG -->
<section class="section" aria-labelledby="kit-h">
<div class="wrap two">
<div class="card kit" id="kit">
<h2 id="kit-h" class="card-h">Build your starter kit</h2>
<p class="card-p">Tick what you need — we bundle it in kraft paper and post it Tuesdays.</p>
<ul class="kit-list" id="kit-list">
<li><label><input type="checkbox" data-price="14.00" checked /><span class="kl-n">Merino wool skein <em>· sage</em></span><span class="kl-p">$14.00</span></label></li>
<li><label><input type="checkbox" data-price="9.50" checked /><span class="kl-n">Bamboo needles <em>· 5 mm</em></span><span class="kl-p">$9.50</span></label></li>
<li><label><input type="checkbox" data-price="6.25" /><span class="kl-n">Embroidery hoop <em>· 6 in</em></span><span class="kl-p">$6.25</span></label></li>
<li><label><input type="checkbox" data-price="11.00" /><span class="kl-n">Beeswax pellets <em>· 500 g</em></span><span class="kl-p">$11.00</span></label></li>
<li><label><input type="checkbox" data-price="18.40" /><span class="kl-n">Stoneware clay <em>· 2 kg</em></span><span class="kl-p">$18.40</span></label></li>
<li><label><input type="checkbox" data-price="7.80" /><span class="kl-n">Linen thread set <em>· 12 shades</em></span><span class="kl-p">$7.80</span></label></li>
</ul>
<div class="basket">
<div class="basket-ic" aria-hidden="true">
<svg viewBox="0 0 48 40" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 14h36l-4 22H10z"/><path d="M16 14 22 4M32 14 26 4"/><path d="M14 20l2 10M24 20v10M34 20l-2 10"/>
</svg>
<span class="basket-badge" id="kit-count">2</span>
</div>
<div class="basket-tot">
<span>Kit total</span>
<b id="kit-total">$23.50</b>
<small id="kit-ship">Free shipping unlocked at $40</small>
</div>
<button class="btn" id="kit-add">Add kit</button>
</div>
</div>
<div class="card makealong" id="makealong">
<span class="chip chip-mustard">Seasonal</span>
<h2 class="card-h">Autumn Makealong</h2>
<p class="card-p">Six slow weeks, one woven wall hanging. New chapter drops each Sunday morning.</p>
<div class="ring-row">
<div class="ring" role="img" aria-label="Week 3 of 6 complete, 50 percent">
<svg viewBox="0 0 120 120">
<circle class="ring-bg" cx="60" cy="60" r="50" />
<circle class="ring-fg" cx="60" cy="60" r="50" id="ring-fg" />
</svg>
<div class="ring-txt"><b>3<span>/6</span></b><small>weeks</small></div>
</div>
<ul class="weeks">
<li class="done">Week 1 · Warping the loom</li>
<li class="done">Week 2 · Plain weave & rya</li>
<li class="now">Week 3 · Soumak texture</li>
<li>Week 4 · Colour blocking</li>
<li>Week 5 · Finishing edges</li>
<li>Week 6 · Mounting & hanging</li>
</ul>
</div>
<div class="makealong-foot">
<div class="faces" aria-hidden="true"><span>NR</span><span>TJ</span><span>AL</span><span>MK</span></div>
<p><b>1,284</b> makers joined</p>
<button class="btn btn-ghost btn-sm" id="join">Join week 3</button>
</div>
</div>
</div>
</section>
<!-- GALLERY -->
<section class="section section-alt" id="gallery" aria-labelledby="gal-h">
<div class="wrap">
<h2 id="gal-h" class="sec-h">Community makes</h2>
<p class="sec-p">Tap any make to see it larger.</p>
<div class="gal" id="gal"><!-- injected --></div>
</div>
</section>
<!-- NEWSLETTER -->
<footer class="foot">
<div class="torn torn-top" aria-hidden="true"></div>
<div class="wrap foot-in">
<div>
<h2 class="card-h">A letter every other Thursday</h2>
<p class="card-p">One tutorial, one maker story, zero noise. Unsubscribe with one tap.</p>
</div>
<form class="nl" id="nl" novalidate>
<label class="sr" for="nl-mail">Email address</label>
<div class="nl-row">
<input id="nl-mail" type="email" name="email" placeholder="[email protected]" autocomplete="email" aria-describedby="nl-msg" />
<button class="btn" type="submit">Subscribe</button>
</div>
<p class="nl-msg" id="nl-msg" role="status"></p>
</form>
</div>
<div class="wrap foot-legal">
<p>© 2026 Little Loom Studio · Grangewood · Fictional shop, real stitches.</p>
<p class="mono">MADE-WITH-HANDS · v3.4</p>
</div>
</footer>
</main>
<!-- LIGHTBOX -->
<div class="lb" id="lb" hidden>
<div class="lb-back" data-close></div>
<figure class="lb-box" role="dialog" aria-modal="true" aria-labelledby="lb-cap">
<div class="lb-art" id="lb-art"></div>
<figcaption id="lb-cap"></figcaption>
<button class="lb-x" id="lb-x" aria-label="Close">×</button>
<button class="lb-nav lb-prev" id="lb-prev" aria-label="Previous make">‹</button>
<button class="lb-nav lb-next" id="lb-next" aria-label="Next make">›</button>
</figure>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Crafts & Handmade Landing
Little Loom Studio is a fictional crafts community and supply shop. The page opens on a blush gradient hero where a small cluster of hand-drawn SVG doodles — scissors, a yarn ball, a paint brush and a threaded needle — drift slowly behind the headline. Below it, six craft tiles (knitting, embroidery, paper, candles, pottery, resin) act as a single-select filter for the tutorial grid, each with its own stroke-drawn icon and a count.
The tutorial cards carry a generated SVG illustration area with a torn-paper bottom edge, a maker chip with initials avatar, a time badge, difficulty expressed in one-to-three yarn-ball icons, and a bookmark toggle. A “Saved only” switch narrows the grid to what you bookmarked. The supplies card is a working mini basket: tick items and the badge count, kit total and the distance-to-free-shipping line all update instantly.
The seasonal makealong band shows week 3 of 6 on an SVG progress ring that animates once it scrolls into view, next to a week-by-week checklist and a participants count. The community gallery expands any make into a focus-trapped lightbox with arrow-key and Escape support, and the footer newsletter validates the address as you type, on blur and on submit.
How this look is built
The soft, handmade feel comes almost entirely from CSS rather than assets: the torn-paper edges
are a repeating radial-gradient used as a mask on a solid strip, so the notches stay crisp at
any width and inherit the section background through a custom property — far cleaner than the old
trick of slicing a PNG zig-zag or stacking rotated pseudo-elements. Card frames use an inset
dashed pseudo-element for a stitched look, and the headline highlight is an irregular
border-radius blob rather than an image. The makealong ring is a single SVG circle whose
stroke-dasharray equals its circumference with the offset animated from full to half, kicked off
by an IntersectionObserver so it only plays when seen. Card art is generated at runtime from a
deterministic sine-hash so every tutorial gets a distinct but stable illustration with zero network
cost. Watch two things: the floating doodles animate transform only (never layout properties), and
every continuous animation is cancelled under prefers-reduced-motion: reduce, including the ring
transition and the card entrance stagger.
Illustrative UI only — always follow real safety guidance when working with tools, electronics, or repairs.