DIY — Robotics Lab Landing
A dark, HUD-styled landing page for Servolab, a fictional robotics lab and kit maker. It pairs an animated SVG robot arm running a looping pick-and-place cycle with a live mono telemetry strip, a four-kit product grid whose cards reveal spec sheets on hover or focus, a clickable four-stage skill path on a glowing rail, a syntax-highlighted firmware card with copy-to-clipboard, a league countdown with a ticking leaderboard, a facilities row and a validated signup footer. Vanilla JS only.
MCP
Code
:root{
--bg:#0d1220;
--bg-2:#0a0f1a;
--panel:#161d2e;
--panel-2:#1b2336;
--cyan:#2fe2d5;
--cyan-d:#17b3a8;
--violet:#8b5cf6;
--amber:#ffb020;
--text:#e6ecf5;
--text-2:#aab6c9;
--muted:#7c8ba3;
--line:rgba(230,236,245,0.10);
--line-2:rgba(47,226,213,0.28);
--ok:#3ddc97;
--danger:#ff6b6b;
--r-sm:6px; --r-md:12px; --r-lg:18px;
--sh-1:0 2px 10px rgba(0,0,0,.35);
--sh-2:0 18px 50px rgba(0,0,0,.5);
--glow:0 0 0 1px rgba(47,226,213,.25), 0 0 28px -8px rgba(47,226,213,.55);
--maxw:1180px;
}
*,*::before,*::after{box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{
margin:0;
font-family:Inter,system-ui,sans-serif;
line-height:1.5;
color:var(--text);
background:
radial-gradient(900px 520px at 78% -8%, rgba(139,92,246,.20), transparent 62%),
radial-gradient(760px 460px at 6% 4%, rgba(47,226,213,.16), transparent 60%),
linear-gradient(180deg,var(--bg) 0%, var(--bg-2) 100%);
background-attachment:fixed;
-webkit-font-smoothing:antialiased;
overflow-x:hidden;
}
img,svg{max-width:100%;}
h1,h2,h3{margin:0;line-height:1.14;letter-spacing:-.02em;}
p{margin:0;}
a{color:inherit;text-decoration:none;}
ul,ol,dl{margin:0;padding:0;list-style:none;}
.scanlines{
position:fixed;inset:0;pointer-events:none;z-index:60;
background:repeating-linear-gradient(180deg, rgba(255,255,255,.028) 0 1px, transparent 1px 3px);
mix-blend-mode:overlay;
}
.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
.mono,.t-val,.t-label,.sku,.chips li,.stage-meta,.fac-tag,.code,.stage-readout,.cd b,.sc,.rk,.kt{
font-family:"JetBrains Mono",ui-monospace,monospace;
}
:where(a,button,input,[tabindex]):focus-visible{
outline:2px solid var(--cyan);outline-offset:3px;border-radius:var(--r-sm);
}
/* HUD brackets */
.hud{position:absolute;inset:0;pointer-events:none;}
.hud i{position:absolute;width:14px;height:14px;border:1.5px solid var(--cyan);opacity:.65;}
.hud i:nth-child(1){top:8px;left:8px;border-right:0;border-bottom:0;}
.hud i:nth-child(2){top:8px;right:8px;border-left:0;border-bottom:0;}
.hud i:nth-child(3){bottom:8px;left:8px;border-right:0;border-top:0;}
.hud i:nth-child(4){bottom:8px;right:8px;border-left:0;border-top:0;}
/* buttons */
.btn{
display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
padding:.62rem 1.1rem;border-radius:999px;border:1px solid transparent;
font-weight:600;font-size:.9rem;cursor:pointer;
transition:transform .16s ease, box-shadow .2s ease, background .2s ease, border-color .2s ease;
}
.btn-primary{background:linear-gradient(135deg,var(--cyan),#4fd8ff);color:#04121a;box-shadow:0 8px 24px -12px rgba(47,226,213,.9);}
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 14px 30px -12px rgba(47,226,213,1);}
.btn-ghost{border-color:var(--line-2);color:var(--text);background:rgba(47,226,213,.06);}
.btn-ghost:hover{background:rgba(47,226,213,.14);transform:translateY(-2px);}
.btn-lg{padding:.82rem 1.5rem;font-size:.98rem;}
.btn-mini{padding:.28rem .7rem;font-size:.74rem;border-color:var(--line-2);background:rgba(47,226,213,.08);color:var(--cyan);font-family:"JetBrains Mono",monospace;border-radius:var(--r-sm);}
.btn-mini:hover{background:rgba(47,226,213,.18);}
.btn:active{transform:translateY(0);}
/* NAV */
.nav{
position:sticky;top:0;z-index:40;
display:flex;align-items:center;gap:1.4rem;
padding:.85rem clamp(1rem,4vw,2.4rem);
background:rgba(13,18,32,.72);backdrop-filter:blur(14px);
border-bottom:1px solid var(--line);
}
.brand{display:flex;align-items:center;gap:.55rem;font-weight:800;letter-spacing:-.02em;font-size:1.06rem;}
.brand-mark{color:var(--cyan);display:grid;place-items:center;filter:drop-shadow(0 0 8px rgba(47,226,213,.55));}
.brand-text em{font-style:normal;color:var(--cyan);}
.nav-links{display:flex;gap:1.4rem;margin-left:auto;font-size:.9rem;color:var(--text-2);}
.nav-links a{position:relative;padding:.2rem 0;transition:color .18s ease;}
.nav-links a::after{content:"";position:absolute;left:0;right:100%;bottom:-2px;height:1.5px;background:var(--cyan);transition:right .22s ease;}
.nav-links a:hover{color:var(--text);}
.nav-links a:hover::after{right:0;}
.nav-cta{display:flex;align-items:center;gap:.8rem;}
.status-pill{
display:inline-flex;align-items:center;gap:.4rem;font-size:.74rem;color:var(--text-2);
font-family:"JetBrains Mono",monospace;border:1px solid var(--line);border-radius:999px;padding:.28rem .6rem;
}
.status-pill .dot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 3px rgba(61,220,151,.16);animation:pulse 2.6s infinite;}
@keyframes pulse{50%{opacity:.35;}}
/* HERO */
.hero{
max-width:var(--maxw);margin:0 auto;
padding:clamp(2.4rem,6vw,4.6rem) clamp(1rem,4vw,2rem) clamp(1.6rem,3vw,2.4rem);
display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(1.6rem,4vw,3rem);align-items:center;
}
.eyebrow{
font-family:"JetBrains Mono",monospace;font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
color:var(--cyan);display:flex;align-items:center;gap:.5rem;margin-bottom:.9rem;
}
.eyebrow .tick{width:8px;height:8px;background:var(--cyan);border-radius:2px;box-shadow:0 0 10px var(--cyan);}
h1{font-size:clamp(2.05rem,4.6vw,3.5rem);font-weight:800;}
.grad{background:linear-gradient(100deg,var(--cyan),var(--violet) 65%,var(--amber));-webkit-background-clip:text;background-clip:text;color:transparent;}
.lead{margin-top:1.1rem;color:var(--text-2);font-size:1.03rem;max-width:52ch;}
.hero-actions{display:flex;gap:.8rem;margin-top:1.6rem;flex-wrap:wrap;}
.hero-facts{display:flex;gap:2rem;margin-top:2.2rem;padding-top:1.4rem;border-top:1px solid var(--line);flex-wrap:wrap;}
.hero-facts b{display:block;font-family:"JetBrains Mono",monospace;font-size:1.4rem;color:var(--cyan);}
.hero-facts span{font-size:.78rem;color:var(--muted);}
.hero-stage{
position:relative;border:1px solid var(--line);border-radius:var(--r-lg);
background:linear-gradient(160deg,var(--panel),#0f1626);
box-shadow:var(--sh-2), var(--glow);padding:1rem 1rem .6rem;overflow:hidden;
}
.hero-stage::before{
content:"";position:absolute;inset:0;
background:
linear-gradient(rgba(47,226,213,.05) 1px, transparent 1px) 0 0/100% 26px,
linear-gradient(90deg, rgba(47,226,213,.05) 1px, transparent 1px) 0 0/26px 100%;
pointer-events:none;
}
.stage-brackets i{position:absolute;width:18px;height:18px;border:1.5px solid var(--violet);opacity:.7;}
.stage-brackets i:nth-child(1){top:10px;left:10px;border-right:0;border-bottom:0;}
.stage-brackets i:nth-child(2){top:10px;right:10px;border-left:0;border-bottom:0;}
.stage-brackets i:nth-child(3){bottom:10px;left:10px;border-right:0;border-top:0;}
.stage-brackets i:nth-child(4){bottom:10px;right:10px;border-left:0;border-top:0;}
.arm{display:block;width:100%;height:auto;position:relative;}
.svg-tag{font-family:"JetBrains Mono",monospace;font-size:9px;letter-spacing:.14em;fill:var(--muted);}
.stage-readout{
display:flex;gap:1.2rem;flex-wrap:wrap;justify-content:space-between;
border-top:1px solid var(--line);margin-top:.4rem;padding:.6rem .3rem .3rem;
font-family:"JetBrains Mono",monospace;font-size:.7rem;color:var(--muted);letter-spacing:.08em;
}
.stage-readout b{color:var(--cyan);font-weight:600;}
/* arm animation */
.j1{transform-origin:210px 236px;animation:j1 6s cubic-bezier(.45,.05,.35,1) infinite;}
.j2{transform-origin:210px 146px;animation:j2 6s cubic-bezier(.45,.05,.35,1) infinite;}
.j3{transform-origin:298px 146px;animation:j3 6s ease-in-out infinite;}
.payload{animation:pay 6s steps(1,end) infinite;}
.belt path{stroke-dasharray:10 30;animation:belt 2.2s linear infinite;}
.sweep{stroke-dashoffset:0;animation:sweep 6s linear infinite;}
@keyframes j1{0%,8%{transform:rotate(-34deg);}30%,42%{transform:rotate(-34deg);}60%,72%{transform:rotate(34deg);}92%,100%{transform:rotate(-34deg);}}
@keyframes j2{0%,10%{transform:rotate(46deg);}32%{transform:rotate(30deg);}58%{transform:rotate(-34deg);}74%{transform:rotate(-20deg);}94%,100%{transform:rotate(46deg);}}
@keyframes j3{0%,18%{transform:translateY(0);}26%{transform:translateY(16px);}34%{transform:translateY(0);}62%{transform:translateY(16px);}70%{transform:translateY(0);}100%{transform:translateY(0);}}
@keyframes pay{0%,26%{opacity:0;}27%,62%{opacity:.92;}63%,100%{opacity:0;}}
@keyframes belt{to{stroke-dashoffset:-40;}}
@keyframes sweep{to{stroke-dashoffset:-120;}}
/* TELEMETRY */
.telemetry{border-block:1px solid var(--line);background:rgba(22,29,46,.6);}
.telemetry-strip{
max-width:var(--maxw);margin:0 auto;display:flex;gap:0;flex-wrap:wrap;
padding:0 clamp(1rem,4vw,2rem);
}
.telemetry-strip li{
flex:1 1 150px;display:flex;flex-direction:column;gap:.24rem;
padding:.9rem 1rem;border-left:1px solid var(--line);
}
.telemetry-strip li:first-child{border-left:0;}
.t-label{font-size:.64rem;letter-spacing:.18em;color:var(--muted);}
.t-val{font-size:1.02rem;font-weight:600;color:var(--text);font-variant-numeric:tabular-nums;}
.t-val.ok{color:var(--ok);}
.t-val.warn{color:var(--amber);}
/* SECTIONS */
.section{max-width:var(--maxw);margin:0 auto;padding:clamp(3rem,7vw,5rem) clamp(1rem,4vw,2rem);}
.sec-head{display:grid;grid-template-columns:1fr 1fr;gap:1.4rem;align-items:end;margin-bottom:2rem;}
h2{font-size:clamp(1.5rem,3vw,2.2rem);font-weight:800;}
.sec-note{color:var(--text-2);font-size:.95rem;max-width:56ch;}
/* KITS */
.kit-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1.1rem;}
.kit{
position:relative;border:1px solid var(--line);border-radius:var(--r-lg);
background:linear-gradient(165deg,var(--panel),#121828);
overflow:hidden;transition:transform .25s ease, border-color .25s ease, box-shadow .25s ease;
}
.kit:hover,.kit:focus-visible,.kit.is-open{transform:translateY(-6px);border-color:var(--line-2);box-shadow:var(--sh-2),var(--glow);}
.kit.is-featured{border-color:rgba(139,92,246,.4);}
.flag{
position:absolute;top:14px;right:14px;z-index:2;font-family:"JetBrains Mono",monospace;
font-size:.62rem;letter-spacing:.12em;text-transform:uppercase;
background:rgba(139,92,246,.18);border:1px solid rgba(139,92,246,.5);color:#c9b6ff;
padding:.24rem .5rem;border-radius:999px;
}
.kit-art{
height:132px;display:grid;place-items:center;color:var(--cyan);
background:
radial-gradient(closest-side, rgba(47,226,213,.16), transparent 72%),
linear-gradient(180deg, rgba(255,255,255,.03), transparent);
border-bottom:1px solid var(--line);
}
.kit-art svg{width:132px;height:98px;filter:drop-shadow(0 0 10px rgba(47,226,213,.35));transition:transform .3s ease;}
.kit:hover .kit-art svg{transform:translateY(-4px) scale(1.05);}
.art-rover{color:var(--violet);}
.art-rover svg{filter:drop-shadow(0 0 10px rgba(139,92,246,.4));}
.art-liner{color:var(--amber);}
.art-liner svg{filter:drop-shadow(0 0 10px rgba(255,176,32,.35));}
.kit-body{padding:1.05rem 1.1rem 1.2rem;}
.kit-badges{display:flex;gap:.4rem;margin-bottom:.6rem;flex-wrap:wrap;}
.badge{font-size:.66rem;font-family:"JetBrains Mono",monospace;letter-spacing:.08em;padding:.2rem .5rem;border-radius:999px;border:1px solid var(--line);color:var(--text-2);}
.b-cyan{color:var(--cyan);border-color:rgba(47,226,213,.4);background:rgba(47,226,213,.08);}
.b-violet{color:#c9b6ff;border-color:rgba(139,92,246,.4);background:rgba(139,92,246,.1);}
.b-amber{color:var(--amber);border-color:rgba(255,176,32,.4);background:rgba(255,176,32,.08);}
.b-lvl{background:rgba(255,255,255,.04);}
.kit h3{font-size:1.1rem;margin-bottom:.4rem;}
.kit-desc{color:var(--text-2);font-size:.87rem;}
.chips{display:flex;gap:.36rem;flex-wrap:wrap;margin-top:.75rem;}
.chips li{font-size:.66rem;color:var(--muted);border:1px dashed var(--line);border-radius:var(--r-sm);padding:.2rem .45rem;}
.kit-foot{display:flex;align-items:baseline;justify-content:space-between;margin-top:.9rem;padding-top:.75rem;border-top:1px solid var(--line);}
.price{font-family:"JetBrains Mono",monospace;font-size:1.24rem;font-weight:700;color:var(--text);}
.sku{font-size:.66rem;color:var(--muted);}
.spec{
display:grid;gap:.35rem;overflow:hidden;max-height:0;opacity:0;
transition:max-height .35s ease, opacity .3s ease, margin-top .3s ease;margin-top:0;
}
.kit:hover .spec,.kit:focus-visible .spec,.kit.is-open .spec{max-height:220px;opacity:1;margin-top:.85rem;}
.spec div{display:flex;justify-content:space-between;gap:1rem;font-size:.76rem;border-bottom:1px dotted var(--line);padding-bottom:.28rem;}
.spec dt{color:var(--muted);}
.spec dd{margin:0;font-family:"JetBrains Mono",monospace;color:var(--text);}
/* SKILL PATH */
.track{position:relative;display:grid;grid-template-columns:repeat(4,1fr);gap:.6rem;padding-top:1.4rem;}
.rail{position:absolute;left:6%;right:6%;top:2.55rem;height:2px;background:var(--line);border-radius:2px;}
.rail-fill{display:block;height:100%;width:12%;background:linear-gradient(90deg,var(--cyan),var(--violet));border-radius:2px;transition:width .38s cubic-bezier(.4,0,.2,1);box-shadow:0 0 12px rgba(47,226,213,.6);}
.stage{
position:relative;z-index:1;background:none;border:0;cursor:pointer;color:var(--text-2);
display:grid;justify-items:center;gap:.3rem;padding:.4rem 0 0;font-family:inherit;
}
.stage-node{
width:38px;height:38px;border-radius:50%;display:grid;place-items:center;
font-family:"JetBrains Mono",monospace;font-size:.78rem;font-weight:700;
background:var(--panel);border:1.5px solid var(--line);color:var(--muted);
transition:all .25s ease;
}
.stage:hover .stage-node{border-color:var(--line-2);color:var(--cyan);}
.stage.is-active .stage-node{background:linear-gradient(135deg,var(--cyan),var(--violet));color:#04121a;border-color:transparent;box-shadow:0 0 22px -4px rgba(47,226,213,.8);}
.stage-name{font-weight:600;font-size:.92rem;}
.stage.is-active .stage-name{color:var(--text);}
.stage-meta{font-size:.66rem;color:var(--muted);}
.stage-panel{
margin-top:1.6rem;border:1px solid var(--line);border-radius:var(--r-lg);
background:linear-gradient(160deg,var(--panel),#111827);padding:1.4rem 1.5rem;box-shadow:var(--sh-1);
}
.stage-panel h3{font-size:1.2rem;margin-bottom:.5rem;color:var(--cyan);}
.stage-panel p{color:var(--text-2);font-size:.95rem;max-width:70ch;}
.sp-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:.6rem;margin-top:1.1rem;}
.sp-list li{
font-size:.8rem;color:var(--text-2);border:1px solid var(--line);border-radius:var(--r-md);
padding:.6rem .75rem;background:rgba(255,255,255,.02);
}
.sp-list li b{display:block;color:var(--text);font-family:"JetBrains Mono",monospace;font-size:.72rem;letter-spacing:.08em;margin-bottom:.2rem;}
/* CODE */
.split{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(1.4rem,4vw,2.6rem);align-items:start;}
.code-card{position:relative;border:1px solid var(--line);border-radius:var(--r-lg);background:#0b101c;overflow:hidden;box-shadow:var(--sh-2);}
.code-bar{display:flex;align-items:center;gap:.7rem;padding:.6rem .9rem;border-bottom:1px solid var(--line);background:rgba(255,255,255,.02);}
.code-file{font-family:"JetBrains Mono",monospace;font-size:.78rem;color:var(--text);}
.code-tag{font-family:"JetBrains Mono",monospace;font-size:.66rem;color:var(--muted);margin-right:auto;}
.code{margin:0;padding:1.1rem 1.2rem;font-size:.78rem;line-height:1.72;overflow-x:auto;color:#c6d2e5;}
.code code{white-space:pre;}
.c-c{color:#5c6b85;font-style:italic;}
.c-k{color:var(--violet);}
.c-t{color:var(--cyan);}
.c-f{color:#7cc7ff;}
.c-s{color:var(--amber);}
.c-n{color:#ff9ec4;}
.code-side .eyebrow{margin-top:.4rem;}
.side-list{display:grid;gap:.7rem;margin-top:1.3rem;}
.side-list li{font-size:.87rem;color:var(--text-2);padding-left:1.1rem;position:relative;}
.side-list li::before{content:"";position:absolute;left:0;top:.55em;width:7px;height:7px;border-radius:2px;background:var(--cyan);box-shadow:0 0 8px rgba(47,226,213,.7);}
.side-list b{color:var(--text);display:block;font-size:.82rem;}
.code-side h2{margin-top:.4rem;margin-bottom:.9rem;}
/* LEAGUE */
.league{display:grid;grid-template-columns:1fr 1fr;gap:1.2rem;}
.league-left{position:relative;border:1px solid var(--line);border-radius:var(--r-lg);padding:1.6rem;background:
radial-gradient(600px 300px at 0% 0%, rgba(139,92,246,.18), transparent 65%), var(--panel);}
.league-left h2{margin-bottom:.7rem;}
.countdown{display:flex;gap:.6rem;margin:1.4rem 0;flex-wrap:wrap;}
.cd{flex:1 1 70px;text-align:center;border:1px solid var(--line);border-radius:var(--r-md);padding:.6rem .3rem;background:rgba(255,255,255,.03);}
.cd b{display:block;font-size:1.5rem;color:var(--cyan);font-variant-numeric:tabular-nums;}
.cd span{font-size:.64rem;color:var(--muted);letter-spacing:.12em;text-transform:uppercase;}
.board{border:1px solid var(--line);border-radius:var(--r-lg);background:var(--panel);overflow:hidden;}
.board-head,.row{display:grid;grid-template-columns:38px 1fr 68px 76px;gap:.5rem;align-items:center;padding:.7rem 1rem;}
.board-head{font-family:"JetBrains Mono",monospace;font-size:.64rem;letter-spacing:.14em;color:var(--muted);text-transform:uppercase;border-bottom:1px solid var(--line);background:rgba(255,255,255,.02);}
.row{border-bottom:1px solid var(--line);font-size:.88rem;transition:background .2s ease;}
.row:last-child{border-bottom:0;}
.row:hover{background:rgba(47,226,213,.06);}
.rk{color:var(--muted);font-size:.8rem;}
.sc{text-align:right;font-weight:700;color:var(--text);font-variant-numeric:tabular-nums;}
.kt{color:var(--muted);font-size:.74rem;}
.rank-1 .rk{color:var(--amber);}.rank-1 .sc{color:var(--amber);}
.rank-2 .rk,.rank-3 .rk{color:var(--cyan);}
.row.bump{animation:bump .6s ease;}
@keyframes bump{0%{background:rgba(47,226,213,.22);}100%{background:transparent;}}
/* FACILITIES */
.fac-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:1rem;}
.fac{border:1px solid var(--line);border-radius:var(--r-lg);padding:1.2rem;background:var(--panel);transition:transform .22s ease,border-color .22s ease;}
.fac:hover{transform:translateY(-4px);border-color:var(--line-2);}
.fac-ico{display:grid;place-items:center;width:38px;height:38px;border-radius:var(--r-md);background:rgba(47,226,213,.1);border:1px solid var(--line-2);color:var(--cyan);font-size:1.1rem;margin-bottom:.8rem;}
.fac h3{font-size:1rem;margin-bottom:.35rem;}
.fac p{font-size:.84rem;color:var(--text-2);}
.fac-tag{display:inline-block;margin-top:.8rem;font-size:.66rem;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:.2rem .55rem;}
/* SIGNUP */
.signup{max-width:var(--maxw);margin:0 auto clamp(2rem,5vw,3.5rem);padding:0 clamp(1rem,4vw,2rem);}
.signup-inner{
position:relative;display:grid;grid-template-columns:1fr 1fr;gap:1.6rem;align-items:center;
border:1px solid var(--line-2);border-radius:var(--r-lg);padding:clamp(1.4rem,4vw,2.4rem);
background:radial-gradient(700px 320px at 100% 0%, rgba(47,226,213,.16), transparent 62%), var(--panel);
box-shadow:var(--sh-2);
}
.signup-form{display:flex;gap:.6rem;flex-wrap:wrap;}
.signup-form input{
flex:1 1 220px;padding:.75rem 1rem;border-radius:999px;border:1px solid var(--line);
background:rgba(13,18,32,.8);color:var(--text);font:inherit;font-size:.92rem;
}
.signup-form input::placeholder{color:var(--muted);}
.signup-form input:focus{outline:none;border-color:var(--cyan);box-shadow:0 0 0 3px rgba(47,226,213,.18);}
.signup-form input[aria-invalid="true"]{border-color:var(--danger);}
.form-msg{flex:1 1 100%;font-size:.78rem;color:var(--muted);min-height:1.1em;font-family:"JetBrains Mono",monospace;}
.form-msg.ok{color:var(--ok);}
.form-msg.err{color:var(--danger);}
/* FOOT */
.foot{
border-top:1px solid var(--line);padding:1.6rem clamp(1rem,4vw,2rem);
max-width:var(--maxw);margin:0 auto;display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;
color:var(--muted);font-size:.78rem;
}
/* TOAST */
.toast{
position:fixed;left:50%;bottom:24px;transform:translate(-50%,140%);
background:var(--panel-2);border:1px solid var(--line-2);color:var(--text);
padding:.7rem 1.1rem;border-radius:999px;font-size:.84rem;z-index:80;
box-shadow:var(--sh-2);transition:transform .34s cubic-bezier(.2,.9,.25,1);
}
.toast.show{transform:translate(-50%,0);}
/* RESPONSIVE */
@media (max-width:960px){
.hero,.split,.league,.signup-inner,.sec-head{grid-template-columns:1fr;}
.sec-head{align-items:start;}
.nav-links{display:none;}
}
@media (max-width:520px){
.nav{gap:.6rem;padding:.7rem 1rem;}
.status-pill{display:none;}
.hero{padding-top:1.8rem;}
.hero-facts{gap:1.2rem;}
.telemetry-strip li{flex:1 1 46%;border-left:0;border-top:1px solid var(--line);}
.track{grid-template-columns:repeat(2,1fr);gap:1rem;}
.rail{display:none;}
.board-head,.row{grid-template-columns:26px 1fr 60px;padding:.6rem .7rem;}
.board-head span:nth-child(3),.row .kt{display:none;}
.code{font-size:.7rem;}
.foot{flex-direction:column;}
.cd b{font-size:1.2rem;}
}
@media (prefers-reduced-motion:reduce){
html{scroll-behavior:auto;}
*,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important;}
.j1{transform:rotate(-20deg);}
.j2{transform:rotate(30deg);}
}/* Servolab — robotics lab landing interactions (vanilla JS) */
(function () {
"use strict";
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- toast ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
function pad(n, len) {
var s = String(Math.floor(n));
while (s.length < len) s = "0" + s;
return s;
}
/* ---------- telemetry ---------- */
var tm = {
servo: document.getElementById("tm-servo"),
batt: document.getElementById("tm-batt"),
up: document.getElementById("tm-up"),
pkt: document.getElementById("tm-pkt"),
temp: document.getElementById("tm-temp"),
link: document.getElementById("tm-link")
};
var hud = {
cycles: document.getElementById("hud-cycles"),
torque: document.getElementById("hud-torque"),
mode: document.getElementById("hud-mode")
};
var t0 = Date.now();
var packets = 18420;
var battery = 92.4;
var temp = 38.2;
var phase = 0;
var cycles = 0;
function tick() {
phase += 0.09;
packets += 3 + Math.floor(Math.random() * 5);
battery = Math.max(61.5, battery - 0.004);
temp += (Math.random() - 0.5) * 0.18;
if (temp < 35.4) temp = 35.4;
if (temp > 44.2) temp = 44.2;
var angle = 64 + Math.sin(phase) * 38;
var up = Math.floor((Date.now() - t0) / 1000);
if (tm.servo) tm.servo.textContent = pad(angle, 3) + "." + pad(Math.abs(angle * 10) % 10, 1) + "°";
if (tm.batt) {
tm.batt.textContent = battery.toFixed(1) + "%";
tm.batt.className = "t-val" + (battery < 70 ? " warn" : "");
}
if (tm.up) {
tm.up.textContent =
pad(up / 3600, 2) + ":" + pad((up % 3600) / 60, 2) + ":" + pad(up % 60, 2);
}
if (tm.pkt) tm.pkt.textContent = pad(packets, 6);
if (tm.temp) {
tm.temp.textContent = temp.toFixed(1) + " °C";
tm.temp.className = "t-val" + (temp > 42 ? " warn" : "");
}
if (tm.link) {
var jitter = Math.random();
tm.link.textContent = jitter > 0.965 ? "RESYNC" : "MESH OK";
tm.link.className = "t-val " + (jitter > 0.965 ? "warn" : "ok");
}
if (hud.torque) hud.torque.textContent = (0.34 + Math.abs(Math.sin(phase)) * 0.42).toFixed(2) + " N·m";
}
tick();
setInterval(tick, 1000);
/* cycle counter — matches the 6s CSS arm loop */
if (!reduced) {
setInterval(function () {
cycles += 1;
if (hud.cycles) hud.cycles.textContent = pad(cycles, 3);
if (hud.mode) hud.mode.textContent = cycles % 7 === 0 ? "RECAL" : "CYCLE";
}, 6000);
} else {
if (hud.mode) hud.mode.textContent = "HOLD";
if (hud.cycles) hud.cycles.textContent = "000";
}
/* ---------- kit cards: tap to open spec on touch/keyboard ---------- */
var kits = document.querySelectorAll(".kit");
Array.prototype.forEach.call(kits, function (card) {
card.addEventListener("click", function () {
var open = card.classList.contains("is-open");
Array.prototype.forEach.call(kits, function (c) { c.classList.remove("is-open"); });
if (!open) {
card.classList.add("is-open");
var name = card.querySelector("h3");
toast("Spec sheet — " + (name ? name.textContent : "kit"));
}
});
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
card.click();
}
});
});
/* ---------- skill path ---------- */
var STAGES = {
1: {
title: "Assemble",
body: "Frame, gearbox and servo horns. You work from a printed exploded map with torque values on every fastener, and finish with a dry-run gait on the bench cradle.",
items: [
["TOOLS", "2.0 / 2.5 mm hex, thread lock, digital caliper"],
["OUTPUT", "Rolling chassis, zero-backlash joints"],
["CHECK", "Coach signs the torque card"]
]
},
2: {
title: "Wire",
body: "SL-Bus daisy chains, power budgeting and strain relief. You crimp your own JST harness, label every node and prove the loop with a bus sniffer before power-up.",
items: [
["TOOLS", "Crimper, multimeter, SL-Bus sniffer"],
["OUTPUT", "Labelled 12-node harness, 5 A budget"],
["CHECK", "Continuity + polarity sweep passes"]
]
},
3: {
title: "Program",
body: "Flash the firmware core, then write the behaviour on top. Sessions cover the 1 kHz control tick, PID tuning by ear, and reading a fault map without guessing.",
items: [
["TOOLS", "arm-none-eabi-gcc, SWD probe, plot bench"],
["OUTPUT", "Custom gait or drive routine in C"],
["CHECK", "60 s autonomous run, no faults"]
]
},
4: {
title: "Compete",
body: "Heat strategy, pit discipline and repairs under a clock. Teams run mock rounds against lab bots, then enter the Harbour Circuit qualifiers as a scored entry.",
items: [
["TOOLS", "Pit crate, spares kit, heat timer"],
["OUTPUT", "Scored league entry + match log"],
["CHECK", "Three clean heats in mock rounds"]
]
}
};
var stageBtns = document.querySelectorAll(".stage");
var spTitle = document.getElementById("sp-title");
var spBody = document.getElementById("sp-body");
var spList = document.getElementById("sp-list");
var railFill = document.getElementById("rail-fill");
var panel = document.getElementById("stage-panel");
function selectStage(n) {
var data = STAGES[n];
if (!data) return;
Array.prototype.forEach.call(stageBtns, function (b) {
var on = b.getAttribute("data-stage") === String(n);
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
if (on && panel) panel.setAttribute("aria-labelledby", b.id);
});
if (railFill) railFill.style.width = (n - 1) / 3 * 88 + 12 + "%";
if (spTitle) spTitle.textContent = data.title;
if (spBody) spBody.textContent = data.body;
if (spList) {
spList.innerHTML = "";
data.items.forEach(function (pair) {
var li = document.createElement("li");
var b = document.createElement("b");
b.textContent = pair[0];
li.appendChild(b);
li.appendChild(document.createTextNode(pair[1]));
spList.appendChild(li);
});
}
}
Array.prototype.forEach.call(stageBtns, function (b, i) {
b.addEventListener("click", function () {
selectStage(parseInt(b.getAttribute("data-stage"), 10));
});
b.addEventListener("keydown", function (e) {
var next = null;
if (e.key === "ArrowRight") next = (i + 1) % stageBtns.length;
if (e.key === "ArrowLeft") next = (i - 1 + stageBtns.length) % stageBtns.length;
if (next === null) return;
e.preventDefault();
stageBtns[next].focus();
selectStage(parseInt(stageBtns[next].getAttribute("data-stage"), 10));
});
});
selectStage(1);
/* ---------- copy code ---------- */
var copyBtn = document.getElementById("copy-btn");
var codeBlock = document.getElementById("code-block");
if (copyBtn && codeBlock) {
copyBtn.addEventListener("click", function () {
var text = codeBlock.innerText;
var done = function () {
copyBtn.textContent = "Copied";
toast("gait_step.c copied to clipboard");
setTimeout(function () { copyBtn.textContent = "Copy"; }, 1800);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallback(text, done); });
} else {
fallback(text, done);
}
});
}
function fallback(text, done) {
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); done(); } catch (e) { toast("Copy blocked by the browser"); }
document.body.removeChild(ta);
}
/* ---------- countdown ---------- */
var target = new Date(Date.now() + (17 * 86400 + 6 * 3600 + 42 * 60 + 18) * 1000);
var cd = {
d: document.getElementById("cd-d"),
h: document.getElementById("cd-h"),
m: document.getElementById("cd-m"),
s: document.getElementById("cd-s")
};
function countdown() {
var left = Math.max(0, Math.floor((target - Date.now()) / 1000));
if (cd.d) cd.d.textContent = pad(left / 86400, 2);
if (cd.h) cd.h.textContent = pad((left % 86400) / 3600, 2);
if (cd.m) cd.m.textContent = pad((left % 3600) / 60, 2);
if (cd.s) cd.s.textContent = pad(left % 60, 2);
}
countdown();
setInterval(countdown, 1000);
/* ---------- leaderboard live scoring ---------- */
var rows = document.querySelectorAll(".board-list .row");
if (rows.length && !reduced) {
setInterval(function () {
var row = rows[Math.floor(Math.random() * rows.length)];
var cell = row.querySelector(".sc");
if (!cell) return;
var val = parseInt(cell.textContent.replace(/\D/g, ""), 10) + Math.floor(Math.random() * 9) + 1;
cell.textContent = String(val).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
row.classList.remove("bump");
void row.offsetWidth;
row.classList.add("bump");
}, 4200);
}
/* ---------- signup ---------- */
var form = document.getElementById("signup-form");
var email = document.getElementById("email");
var msg = document.getElementById("form-msg");
if (form) {
form.addEventListener("submit", function (e) {
e.preventDefault();
var v = (email.value || "").trim();
var ok = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v);
email.setAttribute("aria-invalid", ok ? "false" : "true");
msg.className = "form-msg " + (ok ? "ok" : "err");
msg.textContent = ok
? "Confirmed — bench brief queued for " + v
: "Enter a valid email address to subscribe.";
if (ok) {
toast("Welcome to the bench brief");
form.reset();
} else {
email.focus();
}
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Servolab — Robotics Lab & Kit Works</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>
<div class="scanlines" aria-hidden="true"></div>
<header class="nav">
<a class="brand" href="#top">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 32 32" width="26" height="26">
<rect x="6" y="10" width="20" height="14" rx="4" fill="none" stroke="currentColor" stroke-width="2"/>
<circle cx="12.5" cy="17" r="2.2" fill="currentColor"/>
<circle cx="19.5" cy="17" r="2.2" fill="currentColor"/>
<path d="M16 10V5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="4" r="2" fill="currentColor"/>
</svg>
</span>
<span class="brand-text">Servo<em>lab</em></span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="#kits">Kits</a>
<a href="#path">Skill path</a>
<a href="#code">Firmware</a>
<a href="#league">League</a>
<a href="#lab">Lab</a>
</nav>
<div class="nav-cta">
<span class="status-pill" title="Lab occupancy"><i class="dot"></i> Bench 4 / 6 free</span>
<a class="btn btn-primary" href="#signup">Join the lab</a>
</div>
</header>
<main id="top">
<!-- HERO -->
<section class="hero">
<div class="hero-copy">
<p class="eyebrow"><span class="tick" aria-hidden="true"></span> Kit works · Est. 2019 · Building 12</p>
<h1>Robots you assemble,<br /><span class="grad">wire, and actually beat</span> in competition.</h1>
<p class="lead">Servolab ships open-hardware robot kits with printed wiring maps, a plain-C firmware core, and a nine-week coaching track. Every kit arrives bench-tested, torque-logged and ready for a first pick-and-place inside an afternoon.</p>
<div class="hero-actions">
<a class="btn btn-primary btn-lg" href="#kits">Browse the kits</a>
<a class="btn btn-ghost btn-lg" href="#path">See the skill path</a>
</div>
<ul class="hero-facts">
<li><b>4,180</b><span>kits shipped</span></li>
<li><b>62</b><span>school teams</span></li>
<li><b>9 wk</b><span>to first bout</span></li>
</ul>
</div>
<div class="hero-stage" aria-label="Animated robot arm running a pick and place cycle" role="img">
<div class="stage-brackets" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<svg class="arm" viewBox="0 0 420 320" aria-hidden="true">
<defs>
<linearGradient id="lg-arm" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#2fe2d5"/><stop offset="1" stop-color="#8b5cf6"/>
</linearGradient>
<filter id="fg-glow" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="3.2" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<g class="stage-grid" stroke="rgba(47,226,213,0.14)" stroke-width="1">
<path d="M0 260H420M0 200H420M0 140H420M0 80H420"/>
<path d="M60 0V320M140 0V320M220 0V320M300 0V320M380 0V320"/>
</g>
<!-- conveyor -->
<rect x="24" y="262" width="372" height="16" rx="8" fill="#101828" stroke="rgba(47,226,213,0.3)"/>
<g class="belt" stroke="rgba(230,236,245,0.25)" stroke-width="2" stroke-linecap="round">
<path d="M44 270h14M84 270h14M124 270h14M164 270h14M204 270h14M244 270h14M284 270h14M324 270h14M364 270h14"/>
</g>
<!-- bins -->
<rect class="bin" x="40" y="228" width="70" height="30" rx="5" fill="none" stroke="rgba(139,92,246,0.55)"/>
<rect class="bin" x="312" y="228" width="70" height="30" rx="5" fill="none" stroke="rgba(255,176,32,0.55)"/>
<text x="75" y="222" class="svg-tag" text-anchor="middle">PICK</text>
<text x="347" y="222" class="svg-tag" text-anchor="middle">PLACE</text>
<!-- base -->
<rect x="176" y="236" width="68" height="26" rx="6" fill="#1b2438" stroke="url(#lg-arm)"/>
<circle cx="210" cy="236" r="9" fill="#0d1220" stroke="url(#lg-arm)" stroke-width="2"/>
<g class="rig" filter="url(#fg-glow)">
<g class="j1">
<line x1="210" y1="236" x2="210" y2="146" stroke="url(#lg-arm)" stroke-width="9" stroke-linecap="round"/>
<circle cx="210" cy="146" r="7" fill="#0d1220" stroke="#2fe2d5" stroke-width="2"/>
<g class="j2">
<line x1="210" y1="146" x2="298" y2="146" stroke="url(#lg-arm)" stroke-width="7" stroke-linecap="round"/>
<circle cx="298" cy="146" r="6" fill="#0d1220" stroke="#8b5cf6" stroke-width="2"/>
<g class="j3">
<line x1="298" y1="146" x2="298" y2="192" stroke="#8b5cf6" stroke-width="5" stroke-linecap="round"/>
<g class="grip">
<path d="M290 192v16M306 192v16" stroke="#ffb020" stroke-width="4" stroke-linecap="round"/>
<rect class="payload" x="290" y="196" width="16" height="14" rx="3" fill="#2fe2d5" opacity="0.9"/>
</g>
</g>
</g>
</g>
</g>
<path class="sweep" d="M75 236 C 120 120, 300 108, 347 236" fill="none" stroke="rgba(47,226,213,0.45)" stroke-width="1.5" stroke-dasharray="5 7"/>
</svg>
<div class="stage-readout">
<span>MODE <b id="hud-mode">CYCLE</b></span>
<span>CYCLES <b id="hud-cycles">0</b></span>
<span>TORQUE <b id="hud-torque">0.42 N·m</b></span>
</div>
</div>
</section>
<!-- TELEMETRY -->
<section class="telemetry" aria-label="Live bench telemetry">
<ul class="telemetry-strip">
<li><span class="t-label">SERVO θ</span><span class="t-val" id="tm-servo">064.0°</span></li>
<li><span class="t-label">BATTERY</span><span class="t-val" id="tm-batt">92.4%</span></li>
<li><span class="t-label">UPTIME</span><span class="t-val" id="tm-up">00:00:00</span></li>
<li><span class="t-label">PACKETS</span><span class="t-val" id="tm-pkt">018420</span></li>
<li><span class="t-label">BUS TEMP</span><span class="t-val" id="tm-temp">38.2 °C</span></li>
<li><span class="t-label">LINK</span><span class="t-val ok" id="tm-link">MESH OK</span></li>
</ul>
</section>
<!-- KITS -->
<section class="section" id="kits">
<header class="sec-head">
<div>
<p class="eyebrow">Catalogue 04</p>
<h2>Four kits, one bus protocol</h2>
</div>
<p class="sec-note">Every Servolab kit shares the SL-Bus wiring standard, so a servo horn from the Walker drops straight into the Arm. Hover or focus a card for the full spec sheet.</p>
</header>
<div class="kit-grid" id="kit-grid">
<!-- populated statically below -->
<article class="kit" tabindex="0" data-kit="walker">
<span class="hud" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<div class="kit-art art-walker" aria-hidden="true">
<svg viewBox="0 0 120 90"><g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<rect x="36" y="26" width="48" height="26" rx="6"/><path d="M36 40 24 58 16 74M84 40l12 18 8 16M52 52v18l-8 12M68 52v18l8 12"/>
<circle cx="50" cy="38" r="3.4" fill="currentColor" stroke="none"/><circle cx="70" cy="38" r="3.4" fill="currentColor" stroke="none"/>
</g></svg>
</div>
<div class="kit-body">
<div class="kit-badges"><span class="badge b-cyan">Ages 11+</span><span class="badge b-lvl">Starter</span></div>
<h3>SL-01 Quadwalker</h3>
<p class="kit-desc">Eight-servo gait trainer. Learn inverse kinematics by feel before you touch a line of math.</p>
<ul class="chips"><li>184 parts</li><li>8 servos</li><li>PLA frame</li></ul>
<div class="kit-foot"><span class="price">$129</span><span class="sku">SKU SL-01-A</span></div>
<dl class="spec">
<div><dt>Controller</dt><dd>SL-Core M4 · 120 MHz</dd></div>
<div><dt>Battery</dt><dd>2S 1300 mAh LiPo</dd></div>
<div><dt>Build time</dt><dd>≈ 5 h</dd></div>
<div><dt>Firmware</dt><dd>servolab-gait 2.4</dd></div>
</dl>
</div>
</article>
<article class="kit" tabindex="0" data-kit="rover">
<span class="hud" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<div class="kit-art art-rover" aria-hidden="true">
<svg viewBox="0 0 120 90"><g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<rect x="26" y="30" width="68" height="24" rx="6"/><circle cx="40" cy="64" r="10"/><circle cx="80" cy="64" r="10"/>
<path d="M60 30V16M52 16h16"/><circle cx="44" cy="42" r="3.4" fill="currentColor" stroke="none"/><circle cx="76" cy="42" r="3.4" fill="currentColor" stroke="none"/>
</g></svg>
</div>
<div class="kit-body">
<div class="kit-badges"><span class="badge b-violet">Ages 13+</span><span class="badge b-lvl">Intermediate</span></div>
<h3>SL-02 Field Rover</h3>
<p class="kit-desc">Six-wheel rocker chassis with a ToF mast, odometry encoders and a swappable payload deck.</p>
<ul class="chips"><li>241 parts</li><li>6 motors</li><li>ToF mast</li></ul>
<div class="kit-foot"><span class="price">$189</span><span class="sku">SKU SL-02-R</span></div>
<dl class="spec">
<div><dt>Controller</dt><dd>SL-Core M7 · 400 MHz</dd></div>
<div><dt>Sensors</dt><dd>ToF ×3, IMU, 2× encoder</dd></div>
<div><dt>Build time</dt><dd>≈ 8 h</dd></div>
<div><dt>Firmware</dt><dd>servolab-drive 3.1</dd></div>
</dl>
</div>
</article>
<article class="kit is-featured" tabindex="0" data-kit="arm">
<span class="hud" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<span class="flag">Bench favourite</span>
<div class="kit-art art-arm" aria-hidden="true">
<svg viewBox="0 0 120 90"><g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<rect x="42" y="66" width="36" height="12" rx="4"/><path d="M60 66V38l26-8M86 30l4 20M84 50l-6 10"/>
<path d="M74 60l-4 8M82 62l4 8"/><circle cx="60" cy="38" r="4"/><circle cx="86" cy="30" r="4"/>
</g></svg>
</div>
<div class="kit-body">
<div class="kit-badges"><span class="badge b-cyan">Ages 14+</span><span class="badge b-lvl">Advanced</span></div>
<h3>SL-03 Bench Arm</h3>
<p class="kit-desc">Five-axis desktop arm with a magnetic gripper, hard end-stops and a 480 g payload rating.</p>
<ul class="chips"><li>316 parts</li><li>5 axes</li><li>480 g load</li></ul>
<div class="kit-foot"><span class="price">$264</span><span class="sku">SKU SL-03-B</span></div>
<dl class="spec">
<div><dt>Repeatability</dt><dd>± 0.4 mm</dd></div>
<div><dt>Reach</dt><dd>340 mm</dd></div>
<div><dt>Build time</dt><dd>≈ 12 h</dd></div>
<div><dt>Firmware</dt><dd>servolab-arm 5.0</dd></div>
</dl>
</div>
</article>
<article class="kit" tabindex="0" data-kit="liner">
<span class="hud" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<div class="kit-art art-liner" aria-hidden="true">
<svg viewBox="0 0 120 90"><g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<path d="M14 74c22-6 26-30 46-30s26 18 46 12"/><rect x="44" y="26" width="34" height="18" rx="5"/>
<circle cx="44" cy="52" r="7"/><circle cx="78" cy="52" r="7"/><path d="M52 44v-6M70 44v-6"/>
</g></svg>
</div>
<div class="kit-body">
<div class="kit-badges"><span class="badge b-amber">Ages 9+</span><span class="badge b-lvl">Entry</span></div>
<h3>SL-04 Line Scout</h3>
<p class="kit-desc">Solder-free line follower with a five-eye IR array and a tuning dial for the PID loop.</p>
<ul class="chips"><li>96 parts</li><li>No solder</li><li>5-eye IR</li></ul>
<div class="kit-foot"><span class="price">$74</span><span class="sku">SKU SL-04-L</span></div>
<dl class="spec">
<div><dt>Controller</dt><dd>SL-Nano · 48 MHz</dd></div>
<div><dt>Top speed</dt><dd>0.9 m/s</dd></div>
<div><dt>Build time</dt><dd>≈ 2 h</dd></div>
<div><dt>Firmware</dt><dd>servolab-line 1.8</dd></div>
</dl>
</div>
</article>
</div>
</section>
<!-- SKILL PATH -->
<section class="section" id="path">
<header class="sec-head">
<div>
<p class="eyebrow">Coaching track</p>
<h2>Four stages from box to bout</h2>
</div>
<p class="sec-note">Select a stage to open its detail. Each one ends with a signed-off bench check by a Servolab coach.</p>
</header>
<div class="track" role="tablist" aria-label="Skill path stages">
<div class="rail" aria-hidden="true"><span class="rail-fill" id="rail-fill"></span></div>
<button class="stage is-active" role="tab" aria-selected="true" aria-controls="stage-panel" id="st-1" data-stage="1">
<span class="stage-node">01</span><span class="stage-name">Assemble</span><span class="stage-meta">Weeks 1–2</span>
</button>
<button class="stage" role="tab" aria-selected="false" aria-controls="stage-panel" id="st-2" data-stage="2">
<span class="stage-node">02</span><span class="stage-name">Wire</span><span class="stage-meta">Weeks 3–4</span>
</button>
<button class="stage" role="tab" aria-selected="false" aria-controls="stage-panel" id="st-3" data-stage="3">
<span class="stage-node">03</span><span class="stage-name">Program</span><span class="stage-meta">Weeks 5–7</span>
</button>
<button class="stage" role="tab" aria-selected="false" aria-controls="stage-panel" id="st-4" data-stage="4">
<span class="stage-node">04</span><span class="stage-name">Compete</span><span class="stage-meta">Weeks 8–9</span>
</button>
</div>
<div class="stage-panel" id="stage-panel" role="tabpanel" aria-labelledby="st-1" tabindex="0">
<h3 id="sp-title">Assemble</h3>
<p id="sp-body">Frame, gearbox and servo horns. You work from a printed exploded map with torque values on every fastener, and finish with a dry-run gait on the bench cradle.</p>
<ul class="sp-list" id="sp-list"></ul>
</div>
</section>
<!-- CODE -->
<section class="section split" id="code">
<div class="code-card">
<span class="hud" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<div class="code-bar">
<span class="code-file">gait_step.c</span>
<span class="code-tag">servolab-gait 2.4</span>
<button class="btn btn-mini" id="copy-btn" type="button">Copy</button>
</div>
<pre class="code" id="code-block"><code><span class="c-c">/* SL-01 — one gait step over the SL-Bus */</span>
<span class="c-k">#include</span> <span class="c-s">"slbus.h"</span>
<span class="c-k">static const</span> <span class="c-t">float</span> STRIDE = <span class="c-n">38.0f</span>; <span class="c-c">// mm</span>
<span class="c-t">void</span> <span class="c-f">gait_step</span>(<span class="c-t">Leg</span> *leg, <span class="c-t">float</span> phase) {
<span class="c-t">float</span> lift = <span class="c-f">clampf</span>(<span class="c-f">sinf</span>(phase), <span class="c-n">0.0f</span>, <span class="c-n">1.0f</span>);
<span class="c-t">float</span> x = STRIDE * <span class="c-f">cosf</span>(phase);
<span class="c-t">float</span> z = leg->rest_z - lift * <span class="c-n">18.0f</span>;
<span class="c-k">if</span> (!<span class="c-f">ik_solve</span>(leg, x, z)) {
<span class="c-f">sl_fault</span>(SL_ERR_REACH, leg->id);
<span class="c-k">return</span>;
}
<span class="c-f">sl_write_angle</span>(leg->hip, leg->q1);
<span class="c-f">sl_write_angle</span>(leg->knee, leg->q2);
}</code></pre>
</div>
<div class="code-side">
<p class="eyebrow">Firmware core</p>
<h2>Plain C, no hidden runtime</h2>
<p class="sec-note">The whole gait core is 640 lines you can read in one sitting. No block editor, no proprietary IDE — flash it with the same toolchain you would use for any Cortex-M board, then step it in a debugger.</p>
<ul class="side-list">
<li><b>Deterministic loop</b> 1 kHz control tick with a measured 180 µs worst case.</li>
<li><b>One bus</b> SL-Bus half-duplex, 32 nodes, daisy-chained.</li>
<li><b>Fault map</b> Every servo reports stall, over-temp and reach errors.</li>
<li><b>Open licence</b> MIT firmware, CERN-OHL hardware, published BOM.</li>
</ul>
</div>
</section>
<!-- LEAGUE -->
<section class="section" id="league">
<div class="league">
<div class="league-left">
<span class="hud" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<p class="eyebrow">Servolab League · Round 07</p>
<h2>Harbour Circuit qualifiers</h2>
<p class="sec-note">Autonomous pick-and-place, 180-second heats, three attempts per team. Pit access opens two hours before the first heat.</p>
<div class="countdown" id="countdown" role="timer" aria-live="off">
<div class="cd"><b id="cd-d">00</b><span>days</span></div>
<div class="cd"><b id="cd-h">00</b><span>hrs</span></div>
<div class="cd"><b id="cd-m">00</b><span>min</span></div>
<div class="cd"><b id="cd-s">00</b><span>sec</span></div>
</div>
<a class="btn btn-primary" href="#signup">Register a team</a>
</div>
<div class="board">
<div class="board-head"><span>#</span><span>Team</span><span>Kit</span><span>Score</span></div>
<ol class="board-list">
<li class="row rank-1"><span class="rk">1</span><span class="tm">Harbour Gears</span><span class="kt">SL-03</span><span class="sc">1 482</span></li>
<li class="row rank-2"><span class="rk">2</span><span class="tm">Nine Volt Club</span><span class="kt">SL-02</span><span class="sc">1 431</span></li>
<li class="row rank-3"><span class="rk">3</span><span class="tm">Delta Kestrel</span><span class="kt">SL-03</span><span class="sc">1 390</span></li>
<li class="row"><span class="rk">4</span><span class="tm">Tin Meridian</span><span class="kt">SL-01</span><span class="sc">1 288</span></li>
<li class="row"><span class="rk">5</span><span class="tm">Copper Lark</span><span class="kt">SL-02</span><span class="sc">1 241</span></li>
<li class="row"><span class="rk">6</span><span class="tm">Bellwether AV</span><span class="kt">SL-04</span><span class="sc">1 176</span></li>
</ol>
</div>
</div>
</section>
<!-- LAB -->
<section class="section" id="lab">
<header class="sec-head">
<div><p class="eyebrow">Building 12</p><h2>What is on the floor</h2></div>
<p class="sec-note">Members book benches by the hour. Consumables are on the honour shelf.</p>
</header>
<div class="fac-row">
<div class="fac"><span class="fac-ico">⌁</span><h3>Solder bay</h3><p>Six fume-extracted stations, hot air, microscope rework.</p><span class="fac-tag">6 seats</span></div>
<div class="fac"><span class="fac-ico">⬚</span><h3>Print farm</h3><p>Nine FDM machines plus one resin cell for gripper pads.</p><span class="fac-tag">9 + 1</span></div>
<div class="fac"><span class="fac-ico">◎</span><h3>Test arena</h3><p>4 × 6 m taped field with overhead tracking cameras.</p><span class="fac-tag">24 m²</span></div>
<div class="fac"><span class="fac-ico">⎋</span><h3>Parts wall</h3><p>Binned SL-Bus spares, horns, brackets and idler sets.</p><span class="fac-tag">410 bins</span></div>
</div>
</section>
<!-- SIGNUP -->
<section class="signup" id="signup">
<div class="signup-inner">
<span class="hud" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<div>
<p class="eyebrow">Membership</p>
<h2>Get the bench brief</h2>
<p class="sec-note">One email a fortnight: build logs, firmware notes, league results. No kit spam.</p>
</div>
<form class="signup-form" id="signup-form" novalidate>
<label class="sr-only" for="email">Email address</label>
<input id="email" name="email" type="email" placeholder="[email protected]" autocomplete="email" required />
<button class="btn btn-primary" type="submit">Subscribe</button>
<p class="form-msg" id="form-msg" role="status"></p>
</form>
</div>
</section>
</main>
<footer class="foot">
<p>Servolab Kit Works · Building 12, Harbour Road · Fictional brand for demo purposes.</p>
<p class="mono">BUILD 2026.07.30 · SL-BUS v3 · MIT firmware</p>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Robotics Lab Landing
Servolab is a fictional open-hardware robot kit maker, and this page is its shopfront. The hero puts a five-axis SVG arm on a gridded bench that sweeps continuously between a pick bin and a place bin, grabbing a payload halfway through the cycle. Under it, a mono telemetry strip ticks servo angle, battery, uptime, packet count, bus temperature and mesh link state, with warning colours kicking in when temperature or charge drifts out of band.
The catalogue shows four kits — a quadruped walker, a field rover, a bench arm and a line follower — each drawn as a CSS-tinted SVG glyph inside a card with HUD corner brackets. Age and level badges, part-count chips and a price sit above a spec sheet that expands on hover, on keyboard focus, or on tap for touch devices. Below, the skill path is a four-stage rail: clicking or arrowing across the stages slides a gradient fill along the track and swaps the detail panel with tools, output and the coach sign-off for that stage.
The rest of the page keeps the same instrumentation feel. A firmware card shows a hand-highlighted C gait routine with a copy button that falls back to a hidden textarea when the async clipboard is unavailable. The league band counts down to the next qualifying round while the leaderboard nudges a random team’s score every few seconds with a flash. Everything closes on a facilities row and a signup form with inline validation and a toast.
How this look is built
The arm is a nested SVG group hierarchy where each joint animates its own transform: rotate() around
an explicit transform-origin in user units, so the CSS keyframes compose exactly like real forward
kinematics instead of needing per-frame JS math; the conveyor and the sweep path are just
stroke-dasharray plus an animated stroke-dashoffset, which the compositor handles cheaply. The
blueprint texture, the panel glows and the scanline overlay are all layered gradients with
background-size and repeating-linear-gradient, and the HUD brackets are four absolutely positioned
<i> elements with two borders each — no images, no extra requests. Spec sheets expand with a
max-height + opacity transition rather than display, keeping them focusable and animatable.
Because the arm, belt and status dot animate forever, the prefers-reduced-motion block zeroes every
duration and parks the arm at a neutral pose, and the JS mirrors it by skipping the cycle counter and
the leaderboard ticker so a reduced-motion visitor gets a fully static page.
Illustrative UI only — always follow real safety guidance when working with tools, electronics, or repairs.