Run details
- Model
- z-ai/glm-5.3-flashx-legal
- Route
- OpenRouter API, one user message
- Served by
- Z.AI
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 131,072 output tokens (the model's limit)
- Date
- 2 Nov 2026
- Time
- 12 min 13 s
- Size
- 67 kB
- Tokens
- 98 in, 92,310 out (69,223 of them thinking)
- List price
- $0.12 at today's price ($0.37 in / $1.25 out per M)
- Paid
- $0.12
Exact prompt
Build the landing page for Lumen Cycles, a Copenhagen workshop that builds custom cargo bikes to order. Make it look like a real brand you would trust with 4,000 euros. It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Reply with the file in a single ```html code block.
Source
67 kB 1,149 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>Lumen Cycles — Custom cargo bikes, built to order in Copenhagen</title>
<meta name="description" content="Lumen Cycles braze steel cargo bikes and build birch boxes by hand in Nørrebro, Copenhagen. Built to order, fixed quote, six to eight weeks.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F3EFE4'/%3E%3Ccircle cx='16' cy='16' r='7' fill='%23C4551D'/%3E%3C/svg%3E">
<script>document.documentElement.className += ' js';</script>
<style>
/* ---------- tokens & base ---------- */
:root{
--bone:#F3EFE4; --bone2:#E9E2D0; --ink:#1C1A14; --mut:#6E6858;
--acc:#C4551D; --petrol:#2E4C4C; --moss:#4C5C44;
--line:rgba(28,26,20,.18); --lined:rgba(239,232,216,.22);
--serif:Georgia,'Times New Roman','Iowan Old Style',serif;
--mono:ui-monospace,'SFMono-Regular',Menlo,Consolas,'Liberation Mono',monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth;scroll-padding-top:92px}
body{background:var(--bone);color:var(--ink);font-family:var(--serif);font-size:17px;line-height:1.55;-webkit-font-smoothing:antialiased}
::selection{background:var(--acc);color:var(--bone)}
img,svg{display:block}
a{color:inherit}
button{font:inherit;color:inherit}
.wrap{max-width:1280px;margin-inline:auto;padding-inline:clamp(20px,4vw,48px)}
.lbl{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--mut)}
section{scroll-margin-top:92px}
h1,h2,h3,h4{font-weight:400;letter-spacing:-.02em;line-height:1.08}
:focus-visible{outline:2px solid var(--acc);outline-offset:3px}
/* grain */
.grain{position:fixed;inset:0;z-index:95;pointer-events:none;opacity:.05;mix-blend-mode:multiply}
/* ---------- buttons ---------- */
.btn{display:inline-block;font-family:var(--mono);font-size:12px;letter-spacing:.12em;text-transform:uppercase;
padding:14px 22px;border:1px solid var(--ink);background:transparent;color:var(--ink);cursor:pointer;text-decoration:none;transition:.25s}
.btn:hover{background:var(--ink);color:var(--bone)}
.btn.solid{background:var(--ink);color:var(--bone)}
.btn.solid:hover{background:var(--acc);border-color:var(--acc);color:#F6F1E4}
.btn.acc{background:var(--acc);border-color:var(--acc);color:#F6F1E4}
.btn.acc:hover{background:var(--ink);border-color:var(--ink)}
.tlink{font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--acc);text-decoration:none;border-bottom:1px solid currentColor;padding-bottom:2px;cursor:pointer;background:none;border-inline:0;border-top:0}
.tlink:hover{color:var(--ink)}
/* ---------- header ---------- */
header{position:fixed;inset-inline:0;top:0;z-index:60;transition:.3s}
header .wrap{display:flex;align-items:center;gap:1.6rem;padding-block:14px}
header.sc{background:rgba(243,239,228,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
.brand{text-decoration:none;display:flex;flex-direction:column;line-height:1.15}
.brand .bl{font-weight:700;font-size:1.05rem;letter-spacing:.02em}
.brand .bs{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--mut)}
nav{display:flex;gap:1.7rem;margin-left:auto}
nav a{font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;text-decoration:none;position:relative}
nav a::after{content:"";position:absolute;left:0;bottom:-5px;height:1px;width:0;background:var(--acc);transition:.25s}
nav a:hover::after{width:100%}
.hbtn{margin-left:.4rem}
.mbtn{display:none;margin-left:auto;width:44px;height:40px;border:1px solid var(--ink);background:none;cursor:pointer;position:relative}
.mbtn span{position:absolute;left:11px;right:11px;height:1.5px;background:var(--ink);transition:.3s}
.mbtn span:first-child{top:14px}.mbtn span:last-child{bottom:14px}
body.nav .mbtn span:first-child{transform:translateY(5.5px) rotate(45deg)}
body.nav .mbtn span:last-child{transform:translateY(-5.5px) rotate(-45deg)}
.mmenu{position:fixed;inset:0;z-index:55;background:var(--bone);padding:110px clamp(20px,6vw,48px) 40px;display:flex;flex-direction:column;gap:.4rem;transform:translateY(-102%);transition:transform .4s cubic-bezier(.6,0,.2,1)}
body.nav .mmenu{transform:none}
.mmenu a{font-size:2rem;text-decoration:none;padding:.5rem 0;border-bottom:1px solid var(--line)}
.mmenu .btn{margin-top:1.6rem;align-self:flex-start}
/* ---------- hero ---------- */
.hero{padding-top:150px;padding-bottom:48px;position:relative}
.hero-top{display:grid;grid-template-columns:1.25fr .75fr;gap:2.5rem;align-items:end}
.eyebrow{display:flex;align-items:center;gap:12px;margin-bottom:1.4rem}
.eyebrow::before{content:"";width:34px;height:1px;background:var(--acc)}
.hero h1{font-size:clamp(2.4rem,5.3vw,4.4rem);max-width:16ch}
.hero h1 em{font-style:italic;color:var(--acc)}
.hero-side p{margin-bottom:1.6rem;max-width:38ch}
.hero-cta{display:flex;gap:.8rem;flex-wrap:wrap}
.fine{font-family:var(--mono);font-size:11px;letter-spacing:.04em;color:var(--mut);margin-top:1rem}
.coord{position:absolute;right:14px;top:170px;writing-mode:vertical-rl;font-family:var(--mono);font-size:10px;letter-spacing:.22em;color:var(--mut)}
.herobike{margin-top:2.6rem}
.bike-cap{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-top:.4rem;flex-wrap:wrap}
.lampbtn{display:inline-flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;background:none;border:1px solid var(--line);padding:10px 14px;cursor:pointer}
.lampbtn i{width:10px;height:10px;border-radius:50%;background:var(--bone2);border:1px solid var(--ink);transition:.3s}
.lampbtn[aria-pressed="true"] i{background:#F2B45C;box-shadow:0 0 10px 2px rgba(232,162,76,.75)}
/* ---------- bike svg ---------- */
.bike{width:100%;height:auto}
.bike :is(path,circle,line,rect,ellipse){fill:none;stroke:var(--ink);stroke-linecap:round;stroke-linejoin:round}
.bike text{stroke:none}
.bike .sh{fill:var(--ink);stroke:none;opacity:.08}
.bike .tire{stroke-width:11}
.bike .rim{stroke-width:2.5}
.bike .spk{stroke-width:1.4;opacity:.7}
.bike .rot{stroke-width:2;stroke-dasharray:3 4}
.bike .hub{fill:var(--bone);stroke-width:2}
.bike .case{stroke-width:11}
.bike .core{stroke-width:6.5}
.bike .barp,.bike .post{stroke-width:6}
.bike .sad{fill:var(--ink);stroke-width:2}
.bike .chn{stroke-width:2.4}
.bike .crg{stroke-width:3.5}
.bike .crk{stroke-width:5}
.bike .fend{stroke-width:5}
.bike .kick{stroke-width:4}
.bike .woodp{stroke-width:2.5}
.bike .woodl{stroke-width:1.5;opacity:.85}
.bike .railp{stroke-width:5}
.bike .lbr{stroke-width:3}
.bike .btx{font-family:var(--mono);font-size:13px;letter-spacing:5px;fill:rgba(28,26,20,.55)}
.bike .pd{fill:var(--ink);stroke:none}
.bike .batf{fill:var(--petrol);stroke:var(--ink);stroke-width:2}
.bike .led{fill:#EFE8D8}
.bike .motf{fill:var(--ink)}
.bike .motc{fill:#EFE8D8;stroke:var(--ink);stroke-width:2}
.bike .dsp{fill:var(--ink)}
.bike .lamp{fill:#EFE8D8;stroke-width:2;cursor:pointer;transition:fill .4s}
.bike .lampglow{fill:#E8A24C;stroke:none;opacity:0;transition:opacity .45s}
.bike .pool{fill:#E8A24C;stroke:none;opacity:0;transition:opacity .5s}
.bike .tail{fill:var(--acc);stroke:none}
.bike.lit .lampglow{opacity:.85}
.bike.lit .lamp{fill:#F4B45C}
.bike.lit .pool{opacity:.28}
.bike .al{stroke:var(--ink);stroke-width:1;opacity:.45}
.bike .ac{fill:var(--acc);stroke:none}
.bike .at{font-family:var(--mono);font-size:12px;letter-spacing:.08em;fill:var(--ink);opacity:.85}
@keyframes bkdraw{to{stroke-dashoffset:0}}
@keyframes bkin{to{opacity:1}}
@keyframes spin{to{transform:rotate(360deg)}}
.bike .dr{stroke-dasharray:1 1;stroke-dashoffset:1;animation:bkdraw 1.3s cubic-bezier(.5,.1,.2,1) both}
.bike .df{opacity:0;animation:bkin .7s ease both}
.bike .spokes{transform-box:view-box;animation:spin 28s linear infinite}
.bike.nodr .dr{animation:none;stroke-dashoffset:0}
.bike.nodr .df{animation:none;opacity:1}
/* ---------- ticker ---------- */
.ticker{border-block:1px solid var(--line);overflow:hidden;padding:11px 0}
.tk{display:flex;width:max-content;animation:tick 30s linear infinite}
.tk span{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;white-space:nowrap}
.tk span::after{content:"—";margin:0 1.5rem;opacity:.35}
@keyframes tick{to{transform:translateX(-50%)}}
/* ---------- figures ---------- */
.figs .wrap{display:flex;flex-wrap:wrap}
.fig{flex:1 1 220px;padding:30px 26px;border-left:1px solid var(--line)}
.fig:first-child{border-left:0}
.fig b{display:block;font-weight:400;font-size:clamp(1.9rem,3vw,2.6rem);letter-spacing:-.02em}
.fig span{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut)}
/* ---------- section head ---------- */
.shead{display:grid;grid-template-columns:auto 1fr;gap:1.6rem;align-items:start;max-width:840px;margin-bottom:3rem}
.shead .no{font-family:var(--mono);font-size:12px;color:var(--acc);border-top:1px solid var(--acc);padding-top:10px;min-width:3ch}
.shead h2{font-size:clamp(1.8rem,3.2vw,2.5rem);margin-bottom:.7rem}
.shead p{color:var(--mut);max-width:56ch}
.dark .shead .no{color:var(--acc)}
.dark .shead p{color:rgba(239,232,216,.72)}
/* ---------- bikes ---------- */
#bikes{padding-block:110px 100px}
.mlist{border-top:1px solid var(--line)}
.mrow{width:100%;display:flex;align-items:baseline;gap:1.2rem;padding:20px 10px;border:0;border-bottom:1px solid var(--line);background:none;cursor:pointer;text-align:left;transition:.25s}
.mrow .mno{font-family:var(--mono);font-size:12px;color:var(--mut)}
.mrow .mname{font-family:var(--serif);font-size:1.7rem;letter-spacing:-.02em}
.mrow .mtag{font-style:italic;color:var(--mut)}
.mrow .mfrom{margin-left:auto;font-family:var(--mono);font-size:13px}
.mrow:hover{background:rgba(28,26,20,.04)}
.mrow.on{background:rgba(196,85,29,.07)}
.mrow.on .mno{color:var(--acc)}
.mdetail{display:grid;grid-template-columns:7fr 5fr;gap:clamp(2rem,4vw,3.5rem);margin-top:2.8rem;align-items:start}
.mblurb{font-size:1.15rem;margin-bottom:1.4rem;max-width:48ch}
.spec{display:grid;grid-template-columns:1fr 1fr;gap:0 2rem}
.spec>div{display:flex;justify-content:space-between;gap:1rem;border-bottom:1px solid var(--line);padding:9px 2px}
.spec dt{font-family:var(--mono);font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--mut)}
.spec dd{text-align:right;font-size:15px}
.mcta{display:flex;align-items:center;gap:1.4rem;margin-top:1.6rem;flex-wrap:wrap}
.mprice{font-size:1.25rem}
/* ---------- build ---------- */
#build{background:var(--bone2);padding-block:110px 120px}
.cfg{display:grid;grid-template-columns:minmax(320px,5fr) 7fr;gap:clamp(2rem,4vw,3.5rem);align-items:start}
.cg{border:0;margin:0 0 24px}
.cg legend{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--mut);margin-bottom:10px;display:flex;justify-content:space-between;width:100%;gap:1rem}
.opts{display:grid;gap:8px}
.opt{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;border:1px solid rgba(28,26,20,.28);background:transparent;cursor:pointer;text-align:left;transition:.2s}
.opt .on{flex:1}
.opt .d{font-family:var(--mono);font-size:12px;color:var(--mut)}
.opt:hover{border-color:var(--ink)}
.opt[aria-pressed="true"]{border-color:var(--ink);background:var(--bone);box-shadow:inset 0 0 0 1px var(--ink)}
.mark{width:14px;height:14px;border:1.5px solid var(--ink);border-radius:50%;flex:none;position:relative}
.opt[aria-pressed="true"] .mark::after{content:"";position:absolute;inset:3px;background:var(--acc);border-radius:50%}
.sws{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.sw{width:34px;height:34px;border-radius:50%;border:1px solid rgba(28,26,20,.35);background:var(--c);cursor:pointer;position:relative}
.sw[aria-pressed="true"]::after{content:"";position:absolute;inset:-6px;border:1.5px solid var(--ink);border-radius:50%}
.swname{margin-left:6px}
.cfg-view .bike{background:transparent}
.scap{font-family:var(--mono);font-size:11px;letter-spacing:.08em;color:var(--mut);margin-top:.2rem}
.sum{margin-top:1.4rem;border:1px solid var(--ink);background:var(--bone);padding:22px}
.sum .sr{display:flex;justify-content:space-between;gap:1rem;padding:5px 0;font-size:15px}
.sum .sr b{font-weight:400;font-family:var(--mono);font-size:13px}
.sum .st{display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid var(--ink);margin-top:12px;padding-top:14px}
.sum .st b{font-weight:400;font-size:2rem;letter-spacing:-.02em}
.sum .lead{font-family:var(--mono);font-size:11px;letter-spacing:.08em;color:var(--mut);margin-top:6px}
.sum .btn{width:100%;margin-top:16px;text-align:center}
.sum .fine{margin-top:10px}
/* ---------- workshop ---------- */
#workshop{background:var(--ink);color:#EFE8D8;padding-block:120px 110px}
.dark ::selection{background:var(--acc);color:var(--ink)}
.ws{display:grid;grid-template-columns:7fr 5fr;gap:clamp(2.5rem,5vw,5rem);align-items:start}
.ws .lead{font-size:clamp(1.4rem,2.4vw,1.9rem);line-height:1.25;letter-spacing:-.015em;margin-bottom:1.6rem;max-width:24ch}
.ws p{color:rgba(239,232,216,.82);margin-bottom:1.1rem;max-width:58ch}
.ws blockquote{border-left:2px solid var(--acc);padding:.4rem 0 .4rem 1.4rem;margin:1.8rem 0 0;font-style:italic;font-size:1.25rem;max-width:34ch}
.ws cite{display:block;font-style:normal;font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:rgba(239,232,216,.6);margin-top:.8rem}
.wsmedia{position:relative;padding-top:1rem}
.jig{width:100%;height:auto}
.jigcap{font-family:var(--mono);font-size:11px;letter-spacing:.1em;color:rgba(239,232,216,.55);margin-top:.6rem}
.seal{position:absolute;top:-30px;right:-10px;width:130px;height:130px}
.seal .rot{animation:spin 26s linear infinite;transform-origin:70px 70px;transform-box:view-box}
.seal text{font-family:var(--mono);font-size:11px;letter-spacing:2.5px;fill:#EFE8D8}
.seal .sl{font-family:var(--serif);font-style:italic;font-size:30px;fill:var(--acc);letter-spacing:0}
.prn{list-style:none;margin-top:2.6rem;border-top:1px solid var(--lined)}
.prn li{padding:14px 0 14px 0;border-bottom:1px solid var(--lined);display:grid;gap:2px}
.prn strong{font-weight:400;font-style:italic}
.prn li::before{content:"—";color:var(--acc);margin-right:10px}
.proc{list-style:none;position:relative;margin-top:1rem}
.proc::before{content:"";position:absolute;left:20px;top:10px;bottom:10px;width:1px;background:var(--lined)}
.proc li{display:flex;gap:22px;padding:16px 0;position:relative}
.pn{width:42px;height:42px;border:1px solid var(--lined);border-radius:50%;display:grid;place-items:center;font-family:var(--mono);font-size:12px;background:var(--ink);flex:none;z-index:1}
.pt h4{font-size:1.2rem;margin-bottom:.35rem}
.pt p{color:rgba(239,232,216,.75);max-width:62ch;margin:0}
.pw{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--acc)}
.proc-h{margin:4rem 0 1.4rem;font-size:1.4rem}
/* ---------- visit ---------- */
#visit{padding-block:110px 100px}
.vs{display:grid;grid-template-columns:5fr 7fr;gap:clamp(2.5rem,5vw,4.5rem);align-items:start}
.vlist{margin:1.4rem 0;border-top:1px solid var(--line)}
.vlist div{display:flex;justify-content:space-between;gap:1rem;padding:11px 2px;border-bottom:1px solid var(--line)}
.vlist dt{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut)}
.vlist dd{text-align:right}
.vs ul{list-style:none;margin:1.2rem 0 1.6rem}
.vs ul li{padding:6px 0 6px 22px;position:relative}
.vs ul li::before{content:"";position:absolute;left:0;top:14px;width:12px;height:1px;background:var(--acc)}
.map{width:100%;height:auto;border:1px solid var(--ink)}
.map .rds path{stroke:var(--ink);opacity:.3;stroke-width:1.5;fill:none}
.map .hl{stroke:var(--acc);stroke-width:2.5;fill:none}
.map .loop{stroke:var(--acc);stroke-width:2;fill:none;stroke-dasharray:5 6}
.map .mw{font-family:var(--mono);font-size:10px;letter-spacing:2.5px;fill:#EFE8D8}
.map .mlab{font-family:var(--mono);font-size:11px;letter-spacing:.06em;fill:var(--ink)}
.map .dim{opacity:.55}
.map .dot{fill:var(--acc)}
.map .pulse{fill:var(--acc);opacity:.5;animation:pulse 2.4s ease-out infinite;transform-origin:232px 118px;transform-box:view-box}
.map .cmp line,.map .sc line{stroke:var(--ink);stroke-width:1.5}
.map .cmp text,.map .sc text{font-family:var(--mono);font-size:10px;fill:var(--ink)}
@keyframes pulse{0%{transform:scale(1);opacity:.5}100%{transform:scale(3.4);opacity:0}}
/* ---------- faq / notes ---------- */
#questions{padding-block:110px 90px;background:var(--bone2)}
.qas{max-width:860px}
.qa{border-top:1px solid var(--line)}
.qa:last-child{border-bottom:1px solid var(--line)}
.qa-h{width:100%;display:flex;justify-content:space-between;align-items:center;gap:1rem;padding:20px 6px;background:none;border:0;cursor:pointer;text-align:left;font-family:var(--serif);font-size:1.2rem;letter-spacing:-.01em}
.qa-h .ic{font-family:var(--mono);font-size:18px;color:var(--acc);transition:transform .3s}
.qa.open .ic{transform:rotate(45deg)}
.qa-b{display:grid;grid-template-rows:0fr;transition:grid-template-rows .35s ease}
.qa.open .qa-b{grid-template-rows:1fr}
.qa-b>div{overflow:hidden}
.qa-b p{padding:0 6px 22px;color:var(--mut);max-width:64ch}
#notes{padding-block:100px}
.notes-h{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;flex-wrap:wrap;margin-bottom:1.6rem}
.notes-h h2{font-size:1.6rem}
.note{display:grid;grid-template-columns:160px 1fr;gap:1.5rem;padding:22px 0;border-top:1px solid var(--line)}
.note:last-of-type{border-bottom:1px solid var(--line)}
.note time{font-family:var(--mono);font-size:12px;letter-spacing:.06em;color:var(--mut)}
.note h3{font-size:1.25rem;margin-bottom:.4rem;font-style:italic}
.note p{color:var(--mut);max-width:68ch}
/* ---------- footer ---------- */
footer{background:var(--ink);color:#EFE8D8;padding:90px 0 36px}
.fgrid{display:grid;grid-template-columns:4fr 2fr 2fr 4fr;gap:2.5rem}
.fbrand .fl{font-size:2rem;letter-spacing:-.02em}
.fbrand .fl em{font-style:italic;color:var(--acc)}
footer .lbl{color:rgba(239,232,216,.5)}
footer a{color:#EFE8D8;text-decoration:none}
footer a:hover{color:var(--acc)}
.fcol{margin-top:1.2rem;display:grid;gap:6px;font-size:15px;color:rgba(239,232,216,.82)}
#newsForm{display:flex;gap:10px;margin-top:12px}
#newsForm input{flex:1;background:transparent;border:0;border-bottom:1px solid rgba(239,232,216,.4);color:#EFE8D8;font-family:var(--serif);font-size:16px;padding:10px 2px}
#newsForm input::placeholder{color:rgba(239,232,216,.4)}
#newsForm input:focus{outline:none;border-color:var(--acc)}
footer .btn{border-color:#EFE8D8;color:#EFE8D8}
footer .btn:hover{background:#EFE8D8;color:var(--ink)}
.colophon{margin-top:1rem;font-size:14px;color:rgba(239,232,216,.65);max-width:40ch}
.fbot{margin-top:70px;padding-top:22px;border-top:1px solid var(--lined);display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:rgba(239,232,216,.5)}
/* ---------- modal ---------- */
.modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:20px;visibility:hidden;opacity:0;transition:.3s}
.modal.open{visibility:visible;opacity:1}
.mbg{position:absolute;inset:0;background:rgba(28,26,20,.55)}
.mpanel{position:relative;background:var(--bone);width:min(560px,100%);max-height:92vh;overflow:auto;padding:34px;border:1px solid var(--ink)}
.mx{position:absolute;top:14px;right:14px;width:38px;height:38px;border:1px solid var(--line);background:none;cursor:pointer;font-size:18px;line-height:1}
.mx:hover{background:var(--ink);color:var(--bone)}
.mpanel h3{font-size:1.7rem;margin:.5rem 0 1rem}
.mspec{font-family:var(--mono);font-size:12px;letter-spacing:.04em;color:var(--acc);border:1px dashed rgba(196,85,29,.5);padding:10px 12px;margin-bottom:1.4rem}
.mpanel label{display:block;margin-bottom:16px;font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--mut)}
.mpanel input,.mpanel textarea{display:block;width:100%;margin-top:6px;background:transparent;border:0;border-bottom:1px solid rgba(28,26,20,.4);font-family:var(--serif);font-size:16px;color:var(--ink);padding:9px 2px;letter-spacing:0;text-transform:none}
.mpanel input:focus,.mpanel textarea:focus{outline:none;border-color:var(--acc)}
.mpanel .em{display:block;color:#9A3B12;font-size:10px;letter-spacing:.06em;margin-top:5px;min-height:13px}
.mpanel .btn{width:100%;text-align:center}
.mpanel .fine{margin-top:12px}
#mOk{text-align:left}
.okic{width:52px;height:52px;border:1px solid var(--acc);border-radius:50%;display:grid;place-items:center;margin-bottom:1.2rem}
.okic svg{stroke:var(--acc)}
/* ---------- reveal & motion ---------- */
.js .rv{opacity:0;transform:translateY(16px);transition:opacity .7s ease,transform .7s ease}
.js .rv.in{opacity:1;transform:none}
/* ---------- responsive ---------- */
@media (max-width:1000px){
nav{display:none}
.mbtn{display:block}
.hbtn{display:none}
.hero-top{grid-template-columns:1fr;gap:1.6rem}
.coord{display:none}
.mdetail,.cfg,.ws,.vs{grid-template-columns:1fr}
.cfg-view{order:-1}
.fgrid{grid-template-columns:1fr 1fr}
.seal{top:-14px;right:0;width:104px;height:104px}
}
@media (max-width:640px){
.hero{padding-top:120px}
.bike .ann{display:none}
.fig{flex:1 1 46%;border-left:0;border-top:1px solid var(--line)}
.figs .wrap{padding-block:10px}
.note{grid-template-columns:1fr;gap:.3rem}
.fgrid{grid-template-columns:1fr}
.spec{grid-template-columns:1fr}
.mrow{flex-wrap:wrap}
.mrow .mfrom{margin-left:0;width:100%}
}
@media (prefers-reduced-motion:reduce){
*{animation:none!important;transition:none!important}
.bike .dr{stroke-dashoffset:0}
.bike .df{opacity:1}
.js .rv{opacity:1;transform:none}
}
</style>
</head>
<body id="top">
<svg class="grain" aria-hidden="true"><filter id="gr"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(#gr)"/></svg>
<header id="hdr">
<div class="wrap">
<a class="brand" href="#top">
<span class="bl">Lumen Cycles</span>
<span class="bs">København NV · Est. 2017</span>
</a>
<nav>
<a href="#bikes">Bikes</a>
<a href="#build">Build & price</a>
<a href="#workshop">Workshop</a>
<a href="#visit">Visit</a>
<a href="#questions">Questions</a>
</nav>
<button class="btn hbtn" data-book>Book a test ride</button>
<button class="mbtn" id="menuBtn" aria-label="Menu"><span></span><span></span></button>
</div>
</header>
<div class="mmenu" id="mmenu">
<a href="#bikes">Bikes</a>
<a href="#build">Build & price</a>
<a href="#workshop">Workshop</a>
<a href="#visit">Visit</a>
<a href="#questions">Questions</a>
<button class="btn acc" data-book>Book a test ride</button>
</div>
<main>
<!-- ============ HERO ============ -->
<section class="hero">
<span class="coord">55.70° N · 12.55° E — KØBENHAVN</span>
<div class="wrap">
<div class="hero-top">
<div>
<p class="eyebrow lbl">Lumen Cycles · Ryesgade 19B · Nørrebro</p>
<h1>Cargo bikes built to order — <em>one at a time</em> — in Copenhagen.</h1>
</div>
<div class="hero-side">
<p>We braze the frames, join the boxes and assemble every bike ourselves in a former blacksmith’s on Ryesgade. A fixed quote, six to eight weeks, and a machine made to carry your life for twenty years.</p>
<div class="hero-cta">
<a class="btn solid" href="#build">Build & price</a>
<button class="btn" data-book>Book a test ride</button>
</div>
<p class="fine">Test rides Tuesday–Saturday from the workshop door. Free, no obligation.</p>
</div>
</div>
<div class="herobike" id="heroBike"></div>
<div class="bike-cap">
<span class="lbl">Lumen Last, enamel — bicycle no. 215 is on the jig. Click the lamp.</span>
<button class="lampbtn" id="lampBtn" aria-pressed="false"><i></i>Turn the lamp on</button>
</div>
</div>
</section>
<div class="ticker" aria-hidden="true">
<div class="tk" id="tk"></div>
</div>
<div class="figs">
<div class="wrap">
<div class="fig rv"><b>2017</b><span>First frame on the jig</span></div>
<div class="fig rv"><b id="fBikes">214</b><span>Bikes built, and counting</span></div>
<div class="fig rv"><b>7 wks</b><span>Median lead time</span></div>
<div class="fig rv"><b>5 yr</b><span>Frame & fork warranty</span></div>
</div>
</div>
<!-- ============ BIKES ============ -->
<section id="bikes">
<div class="wrap">
<header class="shead rv">
<span class="no">01</span>
<div>
<h2>The bikes</h2>
<p>Three frames, one idea: a bicycle that carries children, cargo and a decade of Danish weather without complaint. Every one brazed, boxed and built here.</p>
</div>
</header>
<div class="mlist rv">
<button class="mrow on" data-m="last"><span class="mno">01</span><span class="mname">Last</span><span class="mtag">The long John</span><span class="mfrom">from €3,850</span></button>
<button class="mrow" data-m="fri"><span class="mno">02</span><span class="mname">Fri</span><span class="mtag">The compact</span><span class="mfrom">from €3,150</span></button>
<button class="mrow" data-m="havn"><span class="mno">03</span><span class="mname">Havn</span><span class="mtag">The hauler</span><span class="mfrom">from €4,350</span></button>
</div>
<div class="mdetail rv">
<div id="modelBike"></div>
<div>
<p class="mblurb" id="mBlurb"></p>
<dl class="spec" id="mSpec"></dl>
<div class="mcta">
<span class="mprice" id="mPrice"></span>
<button class="btn" id="mConf">Configure this model</button>
</div>
</div>
</div>
</div>
</section>
<!-- ============ BUILD & PRICE ============ -->
<section id="build">
<div class="wrap">
<header class="shead rv">
<span class="no">02</span>
<div>
<h2>Build & price</h2>
<p>Specify the bike. The price you see is the price you pay — a fixed quote, nothing hidden. The €500 deposit books your place on the jig and is refundable until we cut the first tube.</p>
</div>
</header>
<div class="cfg">
<div class="cfg-controls rv">
<fieldset class="cg">
<legend>Model</legend>
<div class="opts">
<button class="opt" data-k="model" data-v="last" aria-pressed="true"><span class="mark"></span><span class="on">Last — the long John</span><span class="d">€3,850</span></button>
<button class="opt" data-k="model" data-v="fri" aria-pressed="false"><span class="mark"></span><span class="on">Fri — the compact</span><span class="d">€3,150</span></button>
<button class="opt" data-k="model" data-v="havn" aria-pressed="false"><span class="mark"></span><span class="on">Havn — the hauler</span><span class="d">€4,350</span></button>
</div>
</fieldset>
<fieldset class="cg">
<legend>Paint <span class="swname" id="paintName"></span></legend>
<div class="sws">
<button class="sw" data-k="paint" data-v="ink" style="--c:#211E17" aria-label="Ink" aria-pressed="true"></button>
<button class="sw" data-k="paint" data-v="bone" style="--c:#E7DFC9" aria-label="Bone"></button>
<button class="sw" data-k="paint" data-v="moss" style="--c:#4C5C44" aria-label="Moss"></button>
<button class="sw" data-k="paint" data-v="enamel" style="--c:#C4551D" aria-label="Enamel"></button>
<button class="sw" data-k="paint" data-v="petrol" style="--c:#2E4C4C" aria-label="Petrol"></button>
</div>
</fieldset>
<fieldset class="cg">
<legend>Cargo box</legend>
<div class="opts">
<button class="opt" data-k="box" data-v="birch" aria-pressed="true"><span class="mark"></span><span class="on">Birch ply, oiled</span><span class="d">incl.</span></button>
<button class="opt" data-k="box" data-v="oak" aria-pressed="false"><span class="mark"></span><span class="on">Oak, oiled</span><span class="d">+ €280</span></button>
<button class="opt" data-k="box" data-v="canvas" aria-pressed="false"><span class="mark"></span><span class="on">Birch + canvas cover</span><span class="d">+ €190</span></button>
</div>
</fieldset>
<fieldset class="cg">
<legend>Drivetrain</legend>
<div class="opts">
<button class="opt" data-k="dt" data-v="nexus" aria-pressed="true"><span class="mark"></span><span class="on">Shimano Nexus 8</span><span class="d">incl.</span></button>
<button class="opt" data-k="dt" data-v="alfine" aria-pressed="false"><span class="mark"></span><span class="on">Shimano Alfine 11 + Gates belt</span><span class="d">+ €450</span></button>
<button class="opt" data-k="dt" data-v="rohloff" aria-pressed="false"><span class="mark"></span><span class="on">Rohloff Speedhub 14</span><span class="d">+ €1,150</span></button>
</div>
</fieldset>
<fieldset class="cg">
<legend>E-assist</legend>
<div class="opts">
<button class="opt" data-k="ea" data-v="none" aria-pressed="true"><span class="mark"></span><span class="on">None — pedal power only</span><span class="d">incl.</span></button>
<button class="opt" data-k="ea" data-v="ep8" aria-pressed="false"><span class="mark"></span><span class="on">Shimano EP801 · 418 Wh</span><span class="d">+ €1,650</span></button>
</div>
</fieldset>
<fieldset class="cg">
<legend>Extras</legend>
<div class="opts">
<button class="opt" data-k="child" data-v="x" aria-pressed="false"><span class="mark"></span><span class="on">Child package — rails & two benches</span><span class="d">+ €180</span></button>
<button class="opt" data-k="tray" data-v="x" aria-pressed="false"><span class="mark"></span><span class="on">Front tray</span><span class="d">+ €90</span></button>
</div>
</fieldset>
</div>
<div class="cfg-view rv">
<div id="cfgBike"></div>
<p class="scap" id="cfgCap"></p>
<div class="sum">
<div id="sumLines"></div>
<div class="st"><span class="lbl">Total, the price you pay</span><b id="tot">€3,850</b></div>
<p class="lead" id="lead"></p>
<button class="btn acc" id="resBtn">Reserve this build — €500 deposit</button>
<p class="fine">Refundable until we cut the first tube. We confirm the specification together before any payment.</p>
</div>
</div>
</div>
</div>
</section>
<!-- ============ WORKSHOP ============ -->
<section id="workshop" class="dark">
<div class="wrap">
<header class="shead rv">
<span class="no">03</span>
<div>
<h2>The workshop</h2>
<p>Ryesgade 19B — a blacksmith’s that stopped shoeing horses in 1952 and started building bicycles in 2017.</p>
</div>
</header>
<div class="ws">
<div class="rv">
<p class="lead">We started Lumen because the cargo bikes we wanted to ride didn’t exist — too heavy, too noisy, too disposable.</p>
<p>In 2017 we took over a blacksmith’s on Ryesgade, swept the floor, and put a frame jig where the forge had been. Mette Sander brazes the frames. Jonas Kjær joins the boxes and mixes the oil. Between us we have built 214 bikes, each one on the same jig, each one for a person whose name we know.</p>
<p>The bikes you see all over this city are mostly designed for a price. We wanted the opposite: a steel frame we file by hand, a box that can be re-planked in twenty years, parts any shop in Denmark can service, and a shape that follows how you actually live — two children, a trade, a week of shopping, a long flat city with weather in it.</p>
<blockquote>
“A cargo bike should disappear under your life, not the other way around.”
<cite>Mette Sander — framebuilder & co-founder</cite>
</blockquote>
<ul class="prn">
<li><strong>Braze, don’t cast.</strong><span>Every joint is filed by hand until the tubes look grown, not glued.</span></li>
<li><strong>Standard parts only.</strong><span>If we can’t stock the spare for ten years, we don’t use the part.</span></li>
<li><strong>One builder, start to finish.</strong><span>Their initials are stamped on your left dropout.</span></li>
<li><strong>Ride it before you take it.</strong><span>Twenty kilometres along the harbour, then we adjust.</span></li>
</ul>
</div>
<div class="wsmedia rv">
<svg class="seal" viewBox="0 0 140 140" aria-hidden="true">
<defs><path id="sealc" d="M70,70 m-52,0 a52,52 0 1,1 104,0 a52,52 0 1,1 -104,0"/></defs>
<g class="rot"><text><textPath href="#sealc">HAND-BUILT IN NØRREBRO · LUMEN CYCLES · EST. 2017 · </textPath></text></g>
<circle cx="70" cy="70" r="32" fill="none" stroke="#EFE8D8" stroke-width="1.2"/>
<text class="sl" x="70" y="81" text-anchor="middle">L</text>
</svg>
<svg class="jig" viewBox="0 0 480 430" aria-hidden="true">
<defs><filter id="jg" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="6"/></filter></defs>
<g fill="none" stroke="#EFE8D8" stroke-width="2" stroke-linecap="round" opacity=".9">
<line x1="238" y1="0" x2="238" y2="54"/>
<path d="M210 54 L266 54 L250 84 L226 84 Z" fill="#1C1A14"/>
<circle cx="238" cy="94" r="10" fill="#E8A24C" stroke="none" filter="url(#jg)"/>
<circle cx="238" cy="94" r="4.5" fill="#F2B45C" stroke="none"/>
<path d="M120 182 L238 150 L200 262 L120 182 Z"/>
<rect x="110" y="172" width="18" height="18"/>
<rect x="230" y="140" width="18" height="18"/>
<rect x="192" y="254" width="18" height="18"/>
<line x1="120" y1="200" x2="120" y2="330"/>
<line x1="238" y1="168" x2="238" y2="262"/>
<line x1="28" y1="330" x2="452" y2="330"/>
<line x1="56" y1="330" x2="56" y2="404"/>
<line x1="424" y1="330" x2="424" y2="404"/>
<line x1="28" y1="404" x2="452" y2="404" opacity=".35"/>
<circle cx="330" cy="288" r="16"/>
<line x1="362" y1="266" x2="396" y2="304"/>
<path d="M352 246 q12 -20 28 -8"/>
</g>
</svg>
<p class="jigcap">THE JIG — FRAME No. 215, TUESDAY MORNING</p>
</div>
</div>
<h3 class="proc-h rv">How a Lumen is made</h3>
<ol class="proc">
<li class="rv"><span class="pn">01</span><div class="pt"><h4>The conversation</h4><p>Thirty minutes, at the workshop or on the phone. We listen more than we talk. No deposit, no obligation.</p><span class="pw">Week 0</span></div></li>
<li class="rv"><span class="pn">02</span><div class="pt"><h4>The specification</h4><p>Geometry, paint, box, gears. You get a fixed quote in writing. The €500 deposit books your place on the jig.</p><span class="pw">Week 1</span></div></li>
<li class="rv"><span class="pn">03</span><div class="pt"><h4>The frame</h4><p>Tubes cut, mitred, brazed and filed. Alignment checked twice — once on the jig, once on the flat table.</p><span class="pw">Weeks 2–4</span></div></li>
<li class="rv"><span class="pn">04</span><div class="pt"><h4>Paint & box</h4><p>Powder coat in the colour you chose, applied two streets away. The box is joined, oiled and signed under the floor.</p><span class="pw">Weeks 4–6</span></div></li>
<li class="rv"><span class="pn">05</span><div class="pt"><h4>Handover</h4><p>Built, cables stretched, ridden 20 km along the harbour, brakes bedded, saddle set to you. Then it’s yours.</p><span class="pw">Weeks 6–8</span></div></li>
</ol>
</div>
</section>
<!-- ============ VISIT ============ -->
<section id="visit">
<div class="wrap">
<header class="shead rv">
<span class="no">04</span>
<div>
<h2>Visit & test rides</h2>
<p>The workshop is five minutes from Sankt Hans Torv. Come and see frames on the jig, wood curls on the floor, and ride Last, Fri or Havn from the door. Tuesdays we braze. Saturdays we talk. Ring ahead if you want the tour.</p>
</div>
</header>
<div class="vs">
<div class="rv">
<dl class="vlist">
<div><dt>Address</dt><dd>Ryesgade 19B, 2200 København N</dd></div>
<div><dt>Tue–Fri</dt><dd>10:00 – 18:00</dd></div>
<div><dt>Saturday</dt><dd>10:00 – 14:00</dd></div>
<div><dt>Sun–Mon</dt><dd>Closed — we’re in the back anyway</dd></div>
<div><dt>Phone</dt><dd>+45 31 62 84 70</dd></div>
<div><dt>Email</dt><dd>hej@lumencycles.dk</dd></div>
</dl>
<ul>
<li>Test bikes of all three models, set up and ready</li>
<li>The jig, the paint shelf and the box offcuts</li>
<li>Thirty minutes, no obligation, no deposit</li>
</ul>
<button class="btn solid" data-book>Book a test ride</button>
<p class="fine">Bring a child if you have one — we have benches and a low box to climb into.</p>
</div>
<div class="rv">
<svg class="map" viewBox="0 0 560 430" role="img" aria-label="Stylised map of Nørrebro with the workshop marked on Ryesgade">
<path d="M436 0 C410 70 470 130 448 210 C430 270 470 340 440 430 L560 430 L560 0 Z" fill="#2E4C4C"/>
<text class="mw" x="512" y="200" transform="rotate(90 512 200)">HAVNEN</text>
<ellipse cx="104" cy="332" rx="92" ry="30" fill="#2E4C4C"/>
<text class="mw" x="66" y="338">SØERNE</text>
<g class="rds">
<path d="M0 96 C90 88 180 70 268 78 C340 84 400 64 452 44"/>
<path d="M60 0 C74 70 96 160 84 240 C76 300 60 370 64 430"/>
<path d="M0 236 C90 226 190 244 286 226"/>
<path d="M180 430 C200 340 220 260 236 180"/>
</g>
<path class="hl" d="M196 26 L236 180"/>
<path class="loop" d="M232 118 C210 190 150 230 128 296 C160 322 220 300 250 268 C280 236 268 180 232 118 Z"/>
<circle class="pulse" cx="232" cy="118" r="6"/>
<circle class="dot" cx="232" cy="118" r="5"/>
<text class="mlab" x="248" y="106">LUMEN — RYESGADE 19B</text>
<text class="mlab dim" x="24" y="66">NØRREBRO</text>
<text class="mlab dim" x="298" y="336">INDRE BY</text>
<text class="mlab" x="330" y="256" opacity=".8">test-ride loop · 6 km</text>
<g class="cmp"><line x1="506" y1="382" x2="506" y2="354"/><path d="M506 348 L500 362 L506 358 L512 362 Z" fill="#1C1A14"/><text x="500" y="344">N</text></g>
<g class="sc"><line x1="24" y1="406" x2="96" y2="406"/><text x="24" y="396">500 m</text></g>
</svg>
</div>
</div>
</div>
</section>
<!-- ============ QUESTIONS ============ -->
<section id="questions">
<div class="wrap">
<header class="shead rv">
<span class="no">05</span>
<div>
<h2>Questions, answered properly</h2>
<p>If yours isn’t here, write to hej@lumencycles.dk — a person answers, usually the same day.</p>
</div>
</header>
<div class="qas rv">
<div class="qa">
<button class="qa-h" aria-expanded="false">Do you deliver outside Copenhagen?<span class="ic">+</span></button>
<div class="qa-b"><div><p>Across Denmark, fully assembled, for €90. Across the EU in a crate, 90% assembled, for €140–260 depending on where you live. Further afield: write to us — we’ve sent bikes to Greenland and the Faroes, and the paperwork is less scary than it looks.</p></div></div>
</div>
<div class="qa">
<button class="qa-h" aria-expanded="false">How much can a Lumen carry?<span class="ic">+</span></button>
<div class="qa-b"><div><p>Last: 80 kg in the box, rider up to 130. Fri: 50 kg in the box. Havn: 120 kg in the box, 250 kg all told. Two children on benches, a week’s shopping, or a career of flat-pack furniture — we’ve built for all three.</p></div></div>
</div>
<div class="qa">
<button class="qa-h" aria-expanded="false">Is the e-assist any good in a Danish winter?<span class="ic">+</span></button>
<div class="qa-b"><div><p>The EP801 is sealed, quiet, and lives at the bottom bracket where the box shelters it. Expect 40–80 km from the 418 Wh battery depending on load — Copenhagen is flat. The bike rides perfectly with the motor off; it adds about 3.5 kg.</p></div></div>
</div>
<div class="qa">
<button class="qa-h" aria-expanded="false">What about service and spare parts?<span class="ic">+</span></button>
<div class="qa-b"><div><p>Every part on a Lumen is a standard that any shop can service. We stock spares for at least ten years after the last bike of a model leaves the workshop. The first service, at 300 km, is included.</p></div></div>
</div>
<div class="qa">
<button class="qa-h" aria-expanded="false">How do payments work?<span class="ic">+</span></button>
<div class="qa-b"><div><p>€500 deposit when you book the jig — refundable until we cut tubes. Half the balance when the frame comes back from paint, the rest at handover, once you’ve ridden it. Bank transfer or card. Monthly plans exist; ask.</p></div></div>
</div>
<div class="qa">
<button class="qa-h" aria-expanded="false">What warranty do I get?<span class="ic">+</span></button>
<div class="qa-b"><div><p>Five years on frame and fork, two years on components, and a standing invitation to bring it back for a check-up every spring. We have never replaced a frame. Touch wood — birch, usually.</p></div></div>
</div>
<div class="qa">
<button class="qa-h" aria-expanded="false">Can I carry children?<span class="ic">+</span></button>
<div class="qa-b"><div><p>Yes. The child package adds rails and two padded benches, rated for two children up to about six years. We test the setup with our own. Reer and Thule seats fit the rails; we fit them for you.</p></div></div>
</div>
</div>
</div>
</section>
<!-- ============ NOTES ============ -->
<section id="notes">
<div class="wrap">
<div class="notes-h rv">
<h2>Workshop notes</h2>
<span class="lbl">Also sent as a letter, four times a year</span>
</div>
<article class="note rv">
<time>12 January 2025</time>
<div><h3>Four centimetres</h3>
<p>We moved the steering assembly on Last 4 cm rearwards. The front wheel tucks further under the box, the turning circle drops below three metres, and the bike finally fits the lift at Nørrebro station. Small number, large consequences.</p></div>
</article>
<article class="note rv">
<time>3 December 2024</time>
<div><h3>On salt</h3>
<p>We stopped offering polished rims. Copenhagen’s winter salt finds every seam and sits in it. Powder coat, stainless spokes and a rinse after the cold months — that’s the honest specification.</p></div>
</article>
<article class="note rv">
<time>18 October 2024</time>
<div><h3>The school-run loop</h3>
<p>Our favourite test route from the door: up Ryesgade, around the lakes, back over Queen Louise’s Bridge through the rush hour. Six kilometres. If a cargo bike feels good there, it will feel good anywhere.</p></div>
</article>
</div>
</section>
</main>
<!-- ============ FOOTER ============ -->
<footer>
<div class="wrap">
<div class="fgrid">
<div class="fbrand">
<p class="fl">Lumen <em>Cycles</em></p>
<p class="colophon">Custom cargo bikes, built to order in Nørrebro, Copenhagen. This page was drawn and written in the workshop. No cookies, no trackers, no investors.</p>
</div>
<div>
<span class="lbl">Visit</span>
<div class="fcol">
<span>Ryesgade 19B</span>
<span>2200 København N</span>
<span>Tue–Fri 10–18</span>
<span>Sat 10–14</span>
</div>
</div>
<div>
<span class="lbl">Write</span>
<div class="fcol">
<a href="mailto:hej@lumencycles.dk">hej@lumencycles.dk</a>
<a href="tel:+4531628470">+45 31 62 84 70</a>
<span>@lumencycles</span>
<span>Danish or English</span>
</div>
</div>
<div>
<span class="lbl">The workshop letter</span>
<p class="colophon" style="margin-top:12px">Four letters a year — what we’re building, what we’re learning, what broke. No noise.</p>
<form id="newsForm" novalidate>
<input id="newsIn" type="email" placeholder="you@example.com" aria-label="Email">
<button class="btn" type="submit">Subscribe</button>
</form>
</div>
</div>
<div class="fbot">
<span>© 2025 Lumen Cycles ApS · CVR 41 27 93 08</span>
<span>Set in Georgia & a monospace · Drawn in-house</span>
</div>
</div>
</footer>
<!-- ============ MODAL ============ -->
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
<div class="mbg" data-close></div>
<div class="mpanel">
<button class="mx" data-close aria-label="Close">✕</button>
<span class="lbl">Test rides & builds</span>
<h3 id="mTitle">Hej — come and ride.</h3>
<p class="mspec" id="mSpecLine" hidden></p>
<form id="bookForm" novalidate>
<label>Name<input id="mName" type="text" autocomplete="name"><span class="em" id="eName"></span></label>
<label>Email<input id="mEmail" type="email" autocomplete="email"><span class="em" id="eEmail"></span></label>
<label>Phone — optional<input id="mPhone" type="tel" autocomplete="tel"></label>
<label>Message<textarea id="mMsg" rows="4"></textarea></label>
<button class="btn acc" type="submit">Send request</button>
<p class="fine">We reply within two working days. Nothing is charged now — we confirm the specification together first.</p>
</form>
<div id="mOk" hidden>
<div class="okic"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M4 12.5 L10 18 L20 6"/></svg></div>
<h3>Tak, <span id="mOkName"></span>.</h3>
<p>Your request is in. We answer everything ourselves, usually the same day. No payment is due until we’ve confirmed the specification together — and you’re always welcome to just turn up on a Saturday.</p>
<button class="btn" data-close>Close</button>
</div>
</div>
</div>
<script>
/* ============ helpers ============ */
const $ = (s, e=document) => e.querySelector(s);
const $$ = (s, e=document) => [...e.querySelectorAll(s)];
const fmt = n => '€' + n.toLocaleString('en-US');
/* ============ data ============ */
const PAINTS = {
ink: { n:'Ink', c:'#211E17' },
bone: { n:'Bone', c:'#E7DFC9' },
moss: { n:'Moss', c:'#4C5C44' },
enamel: { n:'Enamel', c:'#C4551D' },
petrol: { n:'Petrol', c:'#2E4C4C' },
};
const WOOD = {
birch: { fill:'#E3C493', dark:'#C8A262', line:'#8A6A3C' },
oak: { fill:'#C69A5E', dark:'#A67B3E', line:'#6E4F24' },
};
const BOXN = { birch:'Birch ply, oiled', oak:'Oak, oiled', canvas:'Birch + canvas cover' };
const DTN = { nexus:'Shimano Nexus 8', alfine:'Shimano Alfine 11 + Gates belt', rohloff:'Rohloff Speedhub 14' };
const EAN = { none:'None', ep8:'Shimano EP801 · 418 Wh' };
const PRICES = {
model:{ last:3850, fri:3150, havn:4350 },
paint:{ ink:0, bone:140, moss:140, enamel:140, petrol:140 },
box: { birch:0, oak:280, canvas:190 },
dt: { nexus:0, alfine:450, rohloff:1150 },
ea: { none:0, ep8:1650 },
extras:{ child:180, tray:90 },
};
const MODELS = {
last:{ name:'Last', tag:'The long John', price:3850, fx:240, R:104, wb:470, bw:250, bh:150, dp:'moss', db:'birch', de:false,
blurb:'The long John, and the one we build most. A fillet-brazed steel beam, a 100-litre box, and a ride that stays calm with two children and a week of shopping in front of you.',
specs:[['Wheelbase','1,680 mm'],['Box','100 L birch ply'],['Cargo, in the box','80 kg'],['System, maximum','200 kg'],['Wheels','26 × 2.0″'],['Brakes','Shimano XT, 180/160 mm'],['Gears, standard','Shimano Nexus 8'],['Bike weight','~24 kg'],['Rider height','155–195 cm'],['Best for','Two children, daily hauls']] },
fri:{ name:'Fri', tag:'The compact', price:3150, fx:250, R:94, wb:400, bw:186, bh:120, dp:'enamel', db:'birch', de:false,
blurb:'The compact. A shorter wheelbase and a 60-litre box for bike-lane life — school, bakery, station. Light enough to carry up a stair, small enough to park like a normal bike.',
specs:[['Wheelbase','1,430 mm'],['Box','60 L birch ply'],['Cargo, in the box','50 kg'],['System, maximum','160 kg'],['Wheels','24 × 1.9″'],['Brakes','Shimano XT, 160/140 mm'],['Gears, standard','Shimano Nexus 8'],['Bike weight','~21 kg'],['Rider height','150–190 cm'],['Best for','School runs, the station']] },
havn:{ name:'Havn', tag:'The hauler', price:4350, fx:226, R:110, wb:505, bw:290, bh:150, dp:'petrol', db:'oak', de:true,
blurb:'The hauler. A longer beam, a 160-litre oak box and a motor at the bottom bracket. For trade, for cargo, for the loads other bikes only talk about.',
specs:[['Wheelbase','1,800 mm'],['Box','160 L oak, oiled'],['Cargo, in the box','120 kg'],['System, maximum','250 kg'],['Wheels','26 × 2.15″'],['Brakes','Shimano XT, 203/180 mm'],['Gears, standard','Shimano Nexus 8'],['Motor','Shimano EP801 · 418 Wh'],['Bike weight','~26 kg + 3.5'],['Rider height','160–205 cm']] },
};
/* ============ the bicycle, drawn ============ */
function bikeSVG(o = {}) {
const uid = o.id || 'bk' + (bikeSVG._n = (bikeSVG._n || 0) + 1);
const R = o.R ?? 104, gy = 556, wy = gy - R;
const fx = o.fx ?? 240, wb = o.wb ?? 470, rx = fx + wb;
const bw = o.bw ?? 250, bh = o.bh ?? 150;
const bx = fx + 34, bBot = gy - 8, bTop = bBot - bh;
const paint = (PAINTS[o.paint] || PAINTS.ink).c;
const wood = o.box === 'oak' ? WOOD.oak : WOOD.birch;
const canvas = o.box === 'canvas';
const HT = [fx + 64, bTop - 130], HB = [fx + 46, bTop - 52];
const BB = [rx - 170, bTop], SC = [rx - 90, bTop - 86];
const ST = [HT[0] + 18, HT[1] - 72], Sad = [SC[0] - 14, SC[1] - 62];
const fR = R + 12;
const n1 = v => +v.toFixed(1);
const dr = (cls, extra = '') => `class="dr ${cls}" pathLength="1" ${extra}`;
const dTop = `M ${n1(HT[0])} ${n1(HT[1])} Q ${n1((HT[0]+SC[0])/2)} ${n1(HT[1]+10)} ${n1(SC[0])} ${n1(SC[1])}`;
const dDwn = `M ${n1(HB[0])} ${n1(HB[1])} Q ${n1((HB[0]+BB[0])/2)} ${n1((HB[1]+BB[1])/2-6)} ${n1(BB[0])} ${n1(BB[1])}`;
const dHd = `M ${n1(HT[0])} ${n1(HT[1])} L ${n1(HB[0])} ${n1(HB[1])}`;
const dSt = `M ${n1(SC[0])} ${n1(SC[1])} Q ${n1(SC[0]-46)} ${n1(SC[1]+40)} ${n1(BB[0])} ${n1(BB[1])}`;
const dFk = `M ${n1(HB[0])} ${n1(HB[1])} Q ${n1(fx+16)} ${n1(wy-56)} ${fx} ${n1(wy)}`;
const dSy = `M ${n1(SC[0])} ${n1(SC[1])} Q ${n1((SC[0]+rx)/2+6)} ${n1((SC[1]+wy)/2+12)} ${n1(rx)} ${n1(wy)}`;
const dCs = `M ${n1(BB[0])} ${n1(BB[1])} Q ${n1((BB[0]+rx)/2)} ${n1((BB[1]+wy)/2+8)} ${n1(rx)} ${n1(wy)}`;
const dSm = `M ${n1(HT[0])} ${n1(HT[1])} L ${n1(ST[0])} ${n1(ST[1])}`;
const dBr = `M ${n1(ST[0])} ${n1(ST[1])} Q ${n1(ST[0]+16)} ${n1(ST[1]-18)} ${n1(ST[0]+78)} ${n1(ST[1]-10)}`;
const dPo = `M ${n1(SC[0])} ${n1(SC[1])} L ${n1(Sad[0])} ${n1(Sad[1])}`;
const dSa = `M ${n1(Sad[0]-27)} ${n1(Sad[1]+3)} Q ${n1(Sad[0])} ${n1(Sad[1]-13)} ${n1(Sad[0]+29)} ${n1(Sad[1]-3)} Q ${n1(Sad[0]+4)} ${n1(Sad[1]+9)} ${n1(Sad[0]-27)} ${n1(Sad[1]+3)} Z`;
const dKk = `M ${n1(bx+bw*0.52)} ${n1(bBot-4)} L ${n1(bx+bw*0.63)} ${n1(gy-2)}`;
const fdx = n1(fR*0.906), fdy = n1(fR*0.423);
const dFf = `M ${n1(fx-fdx)} ${n1(wy-fdy)} A ${fR} ${fR} 0 0 1 ${n1(fx+fdx)} ${n1(wy-fdy)}`;
const dFr = `M ${n1(rx-fdx)} ${n1(wy-fdy)} A ${fR} ${fR} 0 0 1 ${n1(rx+fdx)} ${n1(wy-fdy)}`;
const dBx = `M ${n1(bx)} ${n1(bTop)} L ${n1(bx+bw)} ${n1(bTop)} L ${n1(bx+bw)} ${n1(bBot-10)} Q ${n1(bx+bw)} ${n1(bBot)} ${n1(bx+bw-12)} ${n1(bBot)} L ${n1(bx+12)} ${n1(bBot)} Q ${n1(bx)} ${n1(bBot)} ${n1(bx)} ${n1(bBot-10)} Z`;
const dCv = `M ${n1(bx-10)} ${n1(bTop+14)} Q ${n1(bx+bw/2)} ${n1(bTop-34)} ${n1(bx+bw+10)} ${n1(bTop+14)} L ${n1(bx+bw+10)} ${n1(bTop+26)} Q ${n1(bx+bw/2)} ${n1(bTop-22)} ${n1(bx-10)} ${n1(bTop+26)} Z`;
const dRl = `M ${n1(bx+10)} ${n1(bTop-2)} Q ${n1(bx+bw/2)} ${n1(bTop-54)} ${n1(bx+bw-10)} ${n1(bTop-2)}`;
const frameD = [dTop, dDwn, dHd, dSt, dFk, dSy, dCs];
const wheel = cx0 => {
let sp = '';
for (let i = 0; i < 6; i++) {
const a = Math.PI/6*i + Math.PI/12, dx = Math.cos(a)*(R-18), dy = Math.sin(a)*(R-18);
sp += `<line ${dr('spk','style="animation-delay:.3s"')} x1="${n1(cx0-dx)}" y1="${n1(wy-dy)}" x2="${n1(cx0+dx)}" y2="${n1(wy+dy)}"/>`;
}
return `<g>
<circle ${dr('tire','style="animation-delay:.05s"')} cx="${cx0}" cy="${n1(wy)}" r="${R}"/>
<circle ${dr('rim','style="animation-delay:.2s"')} cx="${cx0}" cy="${n1(wy)}" r="${R-12}"/>
<g class="spokes" style="transform-origin:${cx0}px ${n1(wy)}px">${sp}</g>
<circle class="df rot" style="animation-delay:.55s" cx="${cx0}" cy="${n1(wy)}" r="26"/>
<circle class="df hub" style="animation-delay:.55s" cx="${cx0}" cy="${n1(wy)}" r="7"/>
</g>`;
};
let slats = '';
for (let i = 1; i <= 3; i++) {
const y = n1(bTop + bh*i/4);
slats += `<line class="df woodl" style="animation-delay:.35s; stroke:${wood.line}" x1="${n1(bx+8)}" y1="${y}" x2="${n1(bx+bw-8)}" y2="${y}"/>`;
}
const straps = [0.28, 0.5, 0.72].map(t => {
const x = n1(bx + bw*t);
return `<line style="animation-delay:.4s" class="df" x1="${x}" y1="${n1(bTop-16)}" x2="${x}" y2="${n1(bBot-6)}"/>
<rect style="animation-delay:.4s" class="df" x="${n1(x-4)}" y="${n1(bTop+18)}" width="8" height="10"/>`;
}).join('');
const ang = Math.atan2(BB[1]-HB[1], BB[0]-HB[0]) * 180/Math.PI;
const anns = o.ann ? [
{x:56, y:150, p:HT, t:'FILLET-BRAZED CROMOR'},
{x:HT[0]+106, y:118, p:[bx+bw/2, bTop], t:'100 L BIRCH PLY BOX'},
{x:SC[0]+80, y:118, p:[Sad[0], Sad[1]-6], t:'HONEY LEATHER SADDLE'},
{x:fx-180, y:598, p:[fx, wy], t:'SHIMANO XT · 180 mm ROTOR'},
{x:rx-130, y:598, p:[rx, wy], t:'ROHLOFF 14-SPEED HUB'},
] : [];
const annStr = anns.length ? `<g class="df ann" style="animation-delay:1.25s">` + anns.map(a =>
`<line class="al" x1="${n1(a.x+8)}" y1="${n1(a.y+5)}" x2="${n1(a.p[0])}" y2="${n1(a.p[1])}"/>
<circle class="ac" cx="${n1(a.p[0])}" cy="${n1(a.p[1])}" r="3.5"/>
<text class="at" x="${a.x}" y="${a.y}">${a.t}</text>`).join('') + `</g>` : '';
const vb = o.vb || `${n1(fx-130)} 140 ${n1(wb+270)} 480`;
return `<svg class="bike${o.draw===false?' nodr':''}" viewBox="${vb}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Lumen cargo bicycle, side view">
<defs><filter id="pg${uid}" x="-80%" y="-80%" width="260%" height="260%"><feGaussianBlur stdDeviation="7"/></filter></defs>
<ellipse class="sh" cx="${n1((fx+rx)/2)}" cy="${gy+10}" rx="${n1(wb/2+72)}" ry="8"/>
<ellipse class="pool" cx="${n1(fx-10)}" cy="${gy-2}" rx="95" ry="9" filter="url(#pg${uid})"/>
<path ${dr('kick','style="animation-delay:.2s"')} d="${dKk}"/>
<g class="df" style="animation-delay:.15s">
<path class="woodp" style="fill:${wood.fill}" d="${dBx}"/>
${slats}
<rect class="woodp" style="fill:${wood.dark}" x="${n1(bx-5)}" y="${n1(bTop-9)}" width="${n1(bw+10)}" height="13" rx="3"/>
${canvas ? '' : `<text class="btx" x="${n1(bx+bw/2)}" y="${n1(bTop+bh*0.62)}" text-anchor="middle">LUMEN</text>`}
</g>
${canvas ? `<g class="df" style="animation-delay:.3s"><path style="fill:#5A5846" d="${dCv}"/>${straps}<text class="btx" style="fill:rgba(239,232,216,.85)" x="${n1(bx+bw/2)}" y="${n1(bTop-4)}" text-anchor="middle">LUMEN</text></g>` : ''}
${o.child ? `<g class="df" style="animation-delay:.4s"><path class="railp" d="${dRl}"/><path class="railp" d="M ${n1(bx+22)} ${n1(bTop)} Q ${n1(bx+bw/2)} ${n1(bTop-40)} ${n1(bx+bw-22)} ${n1(bTop)}"/></g>` : ''}
${o.tray ? `<g class="df" style="animation-delay:.4s"><rect class="woodp" style="fill:${wood.fill}" x="${n1(fx-4)}" y="${n1(bTop+18)}" width="30" height="26" rx="3"/><line class="woodl" x1="${n1(fx-4)}" y1="${n1(bTop+30)}" x2="${n1(fx+26)}" y2="${n1(bTop+30)}"/></g>` : ''}
${wheel(fx)}${wheel(rx)}
<path ${dr('fend','style="animation-delay:.45s"')} d="${dFf}"/>
<path ${dr('fend','style="animation-delay:.45s"')} d="${dFr}"/>
<g>${frameD.map(d => `<path ${dr('case','style="animation-delay:.4s"')} d="${d}"/><path ${dr('core','style="animation-delay:.4s; stroke:${paint}"')} d="${d}"/>`).join('')}</g>
<path ${dr('case','style="animation-delay:.55s"')} d="${dSm}"/><path ${dr('core','style="animation-delay:.55s; stroke:${paint}"')} d="${dSm}"/>
<path ${dr('barp','style="animation-delay:.6s"')} d="${dBr}"/>
<circle class="df pd" style="animation-delay:.7s" cx="${n1(ST[0]+78)}" cy="${n1(ST[1]-10)}" r="4.5"/>
<path ${dr('post','style="animation-delay:.55s"')} d="${dPo}"/>
<path ${dr('sad','style="animation-delay:.65s"')} d="${dSa}"/>
<line ${dr('chn','style="animation-delay:.7s"')} x1="${n1(BB[0]+2)}" y1="${n1(BB[1]-24)}" x2="${n1(rx-3)}" y2="${n1(wy-11)}"/>
<line ${dr('chn','style="animation-delay:.7s"')} x1="${n1(BB[0]+2)}" y1="${n1(BB[1]+22)}" x2="${n1(rx-3)}" y2="${n1(wy+9)}"/>
<circle ${dr('crg','style="animation-delay:.7s"')} cx="${n1(BB[0])}" cy="${n1(BB[1])}" r="26"/>
<circle ${dr('crg','style="animation-delay:.7s"')} cx="${n1(BB[0])}" cy="${n1(BB[1])}" r="9"/>
<line ${dr('crk','style="animation-delay:.75s"')} x1="${n1(BB[0])}" y1="${n1(BB[1])}" x2="${n1(BB[0]+26)}" y2="${n1(BB[1]+52)}"/>
<rect class="df pd" style="animation-delay:.8s" x="${n1(BB[0]+18)}" y="${n1(BB[1]+48)}" width="18" height="8" rx="2"/>
${o.eass ? `<g class="df" style="animation-delay:.85s">
<g transform="rotate(${n1(ang)} ${n1(HB[0])} ${n1(HB[1])})"><rect class="batf" x="${n1(HB[0]+30)}" y="${n1(HB[1]-13)}" width="${n1(wb*0.40)}" height="26" rx="13"/><circle class="led" cx="${n1(HB[0]+48)}" cy="${n1(HB[1])}" r="2.2"/><circle class="led" cx="${n1(HB[0]+60)}" cy="${n1(HB[1])}" r="2.2"/><circle class="led" cx="${n1(HB[0]+72)}" cy="${n1(HB[1])}" r="2.2"/></g>
<circle class="motf" cx="${n1(BB[0])}" cy="${n1(BB[1])}" r="33"/><circle class="motc" cx="${n1(BB[0])}" cy="${n1(BB[1])}" r="11"/>
<rect class="dsp" x="${n1(ST[0]+2)}" y="${n1(ST[1]-24)}" width="26" height="14" rx="3"/>
</g>` : ''}
<g class="df lampg" style="animation-delay:.9s">
<line class="lbr" x1="${n1(fx+52)}" y1="${n1(bTop-90)}" x2="${n1(fx+22)}" y2="${n1(bTop-79)}"/>
<circle class="lampglow" cx="${n1(fx+18)}" cy="${n1(bTop-74)}" r="15" filter="url(#pg${uid})"/>
<circle class="lamp" cx="${n1(fx+18)}" cy="${n1(bTop-74)}" r="7"/>
</g>
<circle class="df tail" style="animation-delay:.9s" cx="${n1((SC[0]+rx)/2)}" cy="${n1((SC[1]+wy)/2)}" r="5"/>
${annStr}
</svg>`;
}
/* ============ hero ============ */
let heroLit = false;
function renderHero() {
const small = matchMedia('(max-width:680px)').matches;
$('#heroBike').innerHTML = bikeSVG({
id:'hero', fx:300, R:104, wb:470, bw:250, bh:150,
paint:'enamel', box:'birch', ann:!small, lit:heroLit,
vb: small ? '150 140 800 480' : '0 92 1120 540'
});
$('#heroBike .bike').classList.toggle('lit', heroLit);
}
function toggleLamp() {
heroLit = !heroLit;
$('#heroBike .bike')?.classList.toggle('lit', heroLit);
const b = $('#lampBtn');
b.setAttribute('aria-pressed', heroLit);
b.innerHTML = heroLit ? '<i></i>Turn the lamp off' : '<i></i>Turn the lamp on';
}
$('#lampBtn').addEventListener('click', toggleLamp);
$('#heroBike').addEventListener('click', e => { if (e.target.classList.contains('lamp')) toggleLamp(); });
let hT; addEventListener('resize', () => { clearTimeout(hT); hT = setTimeout(renderHero, 200); });
/* ============ ticker ============ */
(() => {
const items = ['Built to order','Nørrebro, København','6–8 weeks','Steel · wood · light','Test rides Tue–Sat','Est. 2017'];
$('#tk').innerHTML = [...items, ...items].map(t => `<span>${t}</span>`).join('');
})();
/* ============ models ============ */
let curM = 'last';
function showModel(k) {
curM = k;
const m = MODELS[k];
$$('.mrow').forEach(b => b.classList.toggle('on', b.dataset.m === k));
$('#mBlurb').textContent = m.blurb;
$('#mSpec').innerHTML = m.specs.map(([a,b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join('');
$('#mPrice').textContent = 'From ' + fmt(m.price);
$('#modelBike').innerHTML = bikeSVG({
id:'mdl', fx:m.fx, R:m.R, wb:m.wb, bw:m.bw, bh:m.bh,
paint:m.dp, box:m.db, eass:m.de, vb:`${m.fx-130} 140 ${m.wb+270} 480`
});
}
$$('.mrow').forEach(b => b.addEventListener('click', () => showModel(b.dataset.m)));
$('#mConf').addEventListener('click', () => {
state.model = curM;
syncCfg();
$('#build').scrollIntoView({ behavior:'smooth' });
});
/* ============ configurator ============ */
const state = { model:'last', paint:'ink', box:'birch', dt:'nexus', ea:'none', child:false, tray:false };
let firstCfg = true, curTotal = PRICES.model.last, curSpec = '';
function renderCfg() {
const m = MODELS[state.model];
$('#cfgBike').innerHTML = bikeSVG({
id:'cfg', fx:m.fx, R:m.R, wb:m.wb, bw:m.bw, bh:m.bh,
paint:state.paint, box:state.box, eass:state.ea === 'ep8',
child:state.child, tray:state.tray, draw:firstCfg,
vb:`${m.fx-130} 140 ${m.wb+270} 480`
});
firstCfg = false;
}
function setTotal(v) {
const el = $('#tot'), s = curTotal, t0 = performance.now();
const f = t => {
const p = Math.min(1, (t - t0) / 500), e = 1 - Math.pow(1 - p, 3);
el.textContent = fmt(Math.round(s + (v - s) * e));
if (p < 1) requestAnimationFrame(f);
};
requestAnimationFrame(f);
curTotal = v;
}
function specString(t) {
const m = MODELS[state.model];
return `Lumen ${m.name} · ${PAINTS[state.paint].n} · ${BOXN[state.box]} · ${DTN[state.dt]} · ${EAN[state.ea]}`
+ (state.child ? ' · Child package' : '') + (state.tray ? ' · Front tray' : '')
+ ` — ${fmt(t)}`;
}
function renderSum() {
const rows = [
['Lumen ' + MODELS[state.model].name + ' — ' + MODELS[state.model].tag, PRICES.model[state.model]],
['Paint — ' + PAINTS[state.paint].n, PRICES.paint[state.paint]],
['Box — ' + BOXN[state.box], PRICES.box[state.box]],
['Gears — ' + DTN[state.dt], PRICES.dt[state.dt]],
['Motor — ' + EAN[state.ea], PRICES.ea[state.ea]],
];
if (state.child) rows.push(['Child package', PRICES.extras.child]);
if (state.tray) rows.push(['Front tray', PRICES.extras.tray]);
$('#sumLines').innerHTML = rows.map(([n,p]) => `<div class="sr"><span>${n}</span><b>${p ? fmt(p) : 'incl.'}</b></div>`).join('');
const t = rows.reduce((s,r) => s + r[1], 0);
setTotal(t);
const wks = 7 + (state.ea === 'ep8' ? 1 : 0);
$('#lead').textContent = 'Ready in ~' + wks + ' weeks' + (state.ea === 'ep8' ? ' — the motor adds a week' : '');
curSpec = specString(t);
$('#cfgCap').textContent = curSpec;
}
function syncCfg() {
$$('.opt').forEach(b => {
const k = b.dataset.k;
const on = (k === 'child' || k === 'tray') ? !!state[k] : state[k] === b.dataset.v;
b.setAttribute('aria-pressed', on);
});
$$('.sw').forEach(b => b.setAttribute('aria-pressed', state.paint === b.dataset.v));
$('#paintName').textContent = PAINTS[state.paint].n + (PRICES.paint[state.paint] ? ' · +€' + PRICES.paint[state.paint] : ' · included');
renderCfg();
renderSum();
}
$$('.opt').forEach(b => b.addEventListener('click', () => {
const k = b.dataset.k;
if (k === 'child' || k === 'tray') state[k] = !state[k];
else state[k] = b.dataset.v;
syncCfg();
}));
$$('.sw').forEach(b => b.addEventListener('click', () => { state.paint = b.dataset.v; syncCfg(); }));
$('#resBtn').addEventListener('click', () => openBook(curSpec));
/* ============ modal ============ */
const modal = $('#modal');
function openBook(spec) {
const sl = $('#mSpecLine');
if (spec) { sl.hidden = false; sl.textContent = spec; }
else sl.hidden = true;
$('#mMsg').value = spec
? 'Hej Lumen — I would like to reserve this build:\n' + spec
: 'Hej Lumen — I would like to book a test ride.';
$('#bookForm').hidden = false;
$('#mOk').hidden = true;
modal.classList.add('open');
document.body.style.overflow = 'hidden';
setTimeout(() => $('#mName').focus(), 250);
}
function closeBook() {
modal.classList.remove('open');
document.body.style.overflow = '';
}
$$('[data-book]').forEach(b => b.addEventListener('click', () => openBook(null)));
$$('[data-close]').forEach(b => b.addEventListener('click', closeBook));
addEventListener('keydown', e => { if (e.key === 'Escape') { closeBook(); document.body.classList.remove('nav'); } });
$('#bookForm').addEventListener('submit', e => {
e.preventDefault();
const name = $('#mName').value.trim(), mail = $('#mEmail').value.trim();
let ok = true;
$('#eName').textContent = name.length >= 2 ? '' : 'We would like a name to write back to.';
$('#eEmail').textContent = /^\S+@\S+\.\S+$/.test(mail) ? '' : 'That email does not look quite right.';
if (name.length < 2) ok = false;
if (!/^\S+@\S+\.\S+$/.test(mail)) ok = false;
if (!ok) return;
$('#mOkName').textContent = name.split(' ')[0];
$('#bookForm').hidden = true;
$('#mOk').hidden = false;
});
/* ============ faq ============ */
$$('.qa-h').forEach(b => b.addEventListener('click', () => {
const qa = b.closest('.qa'), open = qa.classList.toggle('open');
b.setAttribute('aria-expanded', open);
}));
/* ============ newsletter ============ */
$('#newsForm').addEventListener('submit', e => {
e.preventDefault();
const v = $('#newsIn').value.trim();
if (!/^\S+@\S+\.\S+$/.test(v)) { $('#newsIn').focus(); return; }
$('#newsForm').outerHTML = '<p style="margin-top:14px;font-style:italic">Tak — the next letter finds you.</p>';
});
/* ============ menu / header ============ */
$('#menuBtn').addEventListener('click', () => document.body.classList.toggle('nav'));
$$('#mmenu a').forEach(a => a.addEventListener('click', () => document.body.classList.remove('nav')));
addEventListener('scroll', () => $('#hdr').classList.toggle('sc', scrollY > 30), { passive:true });
/* ============ reveal & counter ============ */
const io = new IntersectionObserver(es => es.forEach(x => {
if (x.isIntersecting) { x.target.classList.add('in'); io.unobserve(x.target); }
}), { threshold:.12 });
$$('.rv').forEach(el => io.observe(el));
function countUp(el, to, dur = 1400) {
const t0 = performance.now();
const f = t => {
const p = Math.min(1, (t - t0) / dur), e = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(to * e);
if (p < 1) requestAnimationFrame(f);
};
requestAnimationFrame(f);
}
const cio = new IntersectionObserver(es => es.forEach(x => {
if (x.isIntersecting) { countUp(x.target, 214); cio.unobserve(x.target); }
}), { threshold:.6 });
cio.observe($('#fBikes'));
/* ============ init ============ */
renderHero();
showModel('last');
syncCfg();
</script>
</body>
</html>

