Run details
- Model
- z-ai/glm-5.3
- Route
- OpenRouter API, one user message
- Served by
- Decart
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 943,718 output tokens (the model's limit)
- Date
- 24 Oct 2026
- Time
- 9 min
- Size
- 61 kB
- Tokens
- 98 in, 90,501 out (69,378 of them thinking)
- List price
- $0.33 at today's price ($0.08 in / $3.6 out per M)
- Paid
- $0.25
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
61 kB 970 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lumen Cycles — Hand-built cargo bikes, Copenhagen</title>
<meta name="description" content="Made-to-order cargo bikes, hand-brazed in Copenhagen. From €3,890, twelve weeks, ten-year frame warranty.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='22' fill='%23F4F0E6' stroke='%23211C13' stroke-width='6'/%3E%3Cpath d='M32 32 L47 17' stroke='%23D64F12' stroke-width='7' stroke-linecap='round'/%3E%3C/svg%3E">
<style>
/* ============ tokens ============ */
:root{
--paper:#F4F0E6; --paper2:#ECE7D8; --ink:#211C13; --accent:#D64F12; --beam:#E2A63B;
--line:rgba(33,28,19,.16); --soft:rgba(33,28,19,.68); --faint:rgba(33,28,19,.42);
--dark:#1A150E; --pline:rgba(244,240,230,.16);
--serif:'Iowan Old Style','Palatino Linotype',Palatino,'Book Antiqua',Georgia,'Times New Roman',serif;
--mono:'SF Mono','Cascadia Mono',Menlo,Consolas,'Liberation Mono','Courier New',monospace;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:84px;-webkit-tap-highlight-color:transparent}
body{background:var(--paper);color:var(--ink);font:17px/1.62 var(--serif);-webkit-font-smoothing:antialiased;overflow-x:hidden}
body.lock{overflow:hidden}
::selection{background:var(--accent);color:var(--paper)}
img,svg{display:block}
button{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.vh{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
.wrap{max-width:1240px;margin-inline:auto;padding-inline:clamp(18px,4vw,52px)}
.sec{padding-block:clamp(64px,9vw,126px)}
em{font-style:italic}
/* film grain */
.grain{position:fixed;inset:0;z-index:200;pointer-events:none;opacity:.06;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
background-size:160px 160px}
/* ============ type helpers ============ */
.kk{font:11px var(--mono);letter-spacing:.22em;text-transform:uppercase;color:var(--soft);display:flex;align-items:center;gap:12px}
.kk::before{content:"";width:26px;height:1px;background:var(--accent);flex:none}
h1{font-weight:400;font-size:clamp(44px,7vw,86px);line-height:1.02;letter-spacing:-.015em;margin:22px 0 24px}
h2{font-weight:400;font-size:clamp(30px,4vw,46px);line-height:1.08;letter-spacing:-.01em;margin:18px 0 22px}
.lead{font-size:clamp(18px,1.5vw,20px);line-height:1.6;color:var(--soft);max-width:56ch}
.fine{font:11px var(--mono);letter-spacing:.14em;color:var(--faint);text-transform:uppercase}
a{color:inherit}
/* reveal */
.rv{opacity:0;transform:translateY(16px);transition:opacity .7s ease,transform .7s cubic-bezier(.2,.7,.2,1);transition-delay:calc(var(--i,0)*90ms)}
.rv.on{opacity:1;transform:none}
/* ============ buttons ============ */
.btn{display:inline-flex;align-items:center;gap:10px;background:var(--ink);color:var(--paper);border:1px solid var(--ink);
font:11px var(--mono);letter-spacing:.2em;text-transform:uppercase;padding:15px 24px;cursor:pointer;text-decoration:none;
border-radius:2px;transition:background .25s ease,border-color .25s ease,color .25s ease}
.btn svg{transition:transform .25s ease}
.btn:hover{background:var(--accent);border-color:var(--accent);color:#fff}
.btn:hover svg{transform:translateY(2px)}
.btn.ghost{background:transparent;color:var(--ink)}
.btn.ghost:hover{background:var(--ink);border-color:var(--ink);color:var(--paper)}
/* ============ announcement + header ============ */
.annc{background:var(--ink);color:var(--paper);text-align:center;padding:9px 16px;
font:10.5px var(--mono);letter-spacing:.2em;text-transform:uppercase}
.annc b{color:var(--accent);font-weight:400}
.dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 10px 1px 0;animation:pulse 2.4s ease-in-out infinite}
@keyframes pulse{50%{opacity:.25}}
.hd{position:sticky;top:0;z-index:90;background:rgba(244,240,230,.92);backdrop-filter:blur(9px);border-bottom:1px solid transparent;transition:border-color .3s ease}
.hd.sc{border-bottom-color:var(--line)}
.hdw{display:flex;align-items:center;justify-content:space-between;gap:24px;padding-block:16px}
.brand{display:flex;align-items:baseline;gap:10px;text-decoration:none}
.brand svg{width:20px;height:20px;align-self:center}
.brand .bn{font:italic 400 22px var(--serif)}
.brand .bc{font:10px var(--mono);letter-spacing:.3em}
.nl{display:flex;gap:clamp(14px,2.4vw,34px)}
.nl a{font:11px var(--mono);letter-spacing:.18em;text-transform:uppercase;text-decoration:none;color:var(--soft);padding:6px 0;position:relative}
.nl a::after{content:"";position:absolute;left:0;bottom:2px;width:100%;height:1px;background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform .25s ease}
.nl a:hover{color:var(--ink)}
.nl a:hover::after{transform:scaleX(1)}
.mtg{display:none;background:none;border:1px solid var(--line);padding:8px 14px;font:11px var(--mono);letter-spacing:.2em;cursor:pointer}
.mnav{position:absolute;top:100%;left:0;right:0;background:var(--paper);border-bottom:1px solid var(--line);display:none;flex-direction:column;padding:14px clamp(18px,4vw,52px) 20px}
.mnav.open{display:flex}
.mnav a{font:12px var(--mono);letter-spacing:.18em;text-transform:uppercase;text-decoration:none;color:var(--soft);padding:13px 0;border-bottom:1px solid var(--line)}
@media(max-width:920px){.nl,.hd .btn{display:none}.mtg{display:block}}
/* ============ hero ============ */
.hero{padding-top:clamp(44px,7vh,92px)}
.hgrid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,4fr);gap:clamp(28px,6vw,90px);align-items:end}
.cta{display:flex;gap:14px;flex-wrap:wrap;margin-top:34px}
.htx .fine{margin-top:26px}
.stick{border:1px solid var(--line);padding:26px 26px 20px;margin-bottom:8px}
.stick dl div{display:flex;justify-content:space-between;gap:16px;padding:9px 0;border-bottom:1px solid var(--line)}
.stick dl div:last-child{border-bottom:0}
.stick dt{font:10px var(--mono);letter-spacing:.2em;color:var(--faint);text-transform:uppercase;padding-top:3px}
.stick dd{font:14px var(--serif);text-align:right}
.stick p{margin-top:14px;font:italic 13px var(--serif);color:var(--soft)}
.hero-bike{margin-top:clamp(28px,4vw,54px)}
.hero-bike figcaption{display:flex;justify-content:space-between;gap:20px;align-items:baseline;border-top:1px solid var(--line);padding-top:12px;margin-top:6px}
.hero-bike figcaption span{font:10px var(--mono);letter-spacing:.2em;color:var(--faint)}
.hero-bike figcaption em{font:italic 14px var(--serif);color:var(--soft)}
@media(max-width:920px){.hgrid{grid-template-columns:1fr}.stick{display:none}}
/* ============ marquee ============ */
.mq{border-block:1px solid var(--line);overflow:hidden;padding:12px 0;margin-top:clamp(48px,7vw,86px)}
.mq-t{display:inline-flex;white-space:nowrap;animation:mq 46s linear infinite}
.mq:hover .mq-t{animation-play-state:paused}
.mq-t span{font:11px var(--mono);letter-spacing:.26em;color:var(--soft)}
@keyframes mq{to{transform:translateX(-50%)}}
/* ============ stats band ============ */
.stats{display:flex;border-bottom:1px solid var(--line);flex-wrap:wrap}
.stat{flex:1 1 200px;padding:clamp(26px,3vw,44px) clamp(18px,2.5vw,40px);border-left:1px solid var(--line)}
.stat:first-child{flex-grow:1.35;border-left:0;padding-left:0}
.stat b{display:block;font:400 clamp(44px,4.6vw,64px)/1 var(--serif);letter-spacing:-.02em}
.stat span{display:block;font:10px var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--faint);margin-top:12px}
@media(max-width:720px){.stat{flex:1 1 46%;padding-left:18px}.stat:nth-child(odd){border-left:0}}
/* ============ manifesto ============ */
.mani{display:grid;grid-template-columns:minmax(0,6fr) minmax(0,5fr);gap:clamp(30px,6vw,100px)}
.mani h2{font-size:clamp(32px,3.6vw,48px)}
.mani .body p{color:var(--soft);margin-bottom:20px;max-width:58ch}
.press{font:10.5px var(--mono);letter-spacing:.1em;color:var(--faint);border-top:1px solid var(--line);padding-top:16px;line-height:2.1}
.press i{font-style:normal;color:var(--accent)}
@media(max-width:920px){.mani{grid-template-columns:1fr}}
/* ============ frame section ============ */
.fgrid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(28px,5vw,80px);align-items:start;margin-top:44px}
figure.geo{border:1px solid var(--line);padding:clamp(14px,2vw,26px)}
figure.geo .tag{font:10px var(--mono);letter-spacing:.2em;color:var(--faint);display:block;margin-bottom:8px}
figure.geo figcaption{font:10px var(--mono);letter-spacing:.16em;color:var(--faint);border-top:1px solid var(--line);padding-top:10px;margin-top:8px}
.spec div{display:grid;grid-template-columns:118px 1fr;gap:18px;padding:13px 0;border-bottom:1px solid var(--line)}
.spec dt{font:10px var(--mono);letter-spacing:.18em;color:var(--faint);text-transform:uppercase;padding-top:5px}
.spec dd{font-size:16.5px}
.pull{border-left:2px solid var(--accent);padding:6px 0 6px clamp(20px,3vw,36px);margin-top:clamp(36px,5vw,64px);
font:italic clamp(19px,2vw,23px)/1.5 var(--serif);max-width:54ch;color:var(--ink)}
@media(max-width:920px){.fgrid{grid-template-columns:1fr}}
/* ============ configurator ============ */
#configure .intro{font:italic clamp(17px,1.6vw,20px) var(--serif);color:var(--soft);max-width:52ch;margin-top:-6px}
.cfg{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,6fr);gap:clamp(30px,5vw,76px);margin-top:46px;align-items:start}
@media(min-width:1000px){.cfg-l{position:sticky;top:96px}}
.panel{border:1px solid var(--line);padding:clamp(12px,1.6vw,22px)}
.panel .tag{font:10px var(--mono);letter-spacing:.2em;color:var(--faint);display:block;margin-bottom:4px}
.bs{border:1px solid var(--line);padding:22px 24px;margin-top:18px}
.bs-h{display:flex;justify-content:space-between;font:10px var(--mono);letter-spacing:.2em;color:var(--faint);text-transform:uppercase;border-bottom:1px solid var(--line);padding-bottom:12px}
.bs-r{display:flex;align-items:baseline;font-size:15px;padding:7px 0}
.bs-r .ld{flex:1;border-bottom:1px dotted var(--faint);margin:0 8px;transform:translateY(-4px)}
.bs-r b{font:13px var(--mono);font-weight:400;white-space:nowrap}
.bs-t{display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid var(--ink);margin-top:12px;padding-top:14px}
.bs-t span{font:10.5px var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--soft)}
.bs-t b{font:400 clamp(26px,2.6vw,32px) var(--serif);font-variant-numeric:tabular-nums}
.bs-w{font:10.5px var(--mono);letter-spacing:.12em;color:var(--faint);margin-top:6px}
.bs .btn{width:100%;justify-content:center;margin-top:18px}
.bs-n{font:italic 12.5px var(--serif);color:var(--faint);text-align:center;margin-top:12px}
/* option groups */
.gh{display:flex;justify-content:space-between;align-items:baseline;gap:16px;margin:38px 0 6px}
.gh:first-child{margin-top:0}
.gh b{font:11px var(--mono);letter-spacing:.24em;font-weight:400;text-transform:uppercase}
.gh span{font:10.5px var(--mono);letter-spacing:.12em;color:var(--accent);text-transform:uppercase;text-align:right}
.sws{display:flex;gap:clamp(10px,1.4vw,20px);flex-wrap:wrap;margin-top:12px}
.swb{background:none;border:0;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:9px;width:74px;padding:4px 2px}
.sw{width:36px;height:36px;border-radius:50%;border:1px solid rgba(33,28,19,.3);transition:transform .2s ease}
.swb:hover .sw{transform:scale(1.1)}
.swb[aria-pressed="true"] .sw{outline:1.5px solid var(--ink);outline-offset:5px}
.swb .snm{font:9.5px var(--mono);letter-spacing:.06em;color:var(--soft);text-align:center;line-height:1.35}
.swb .spd{font:9px var(--mono);color:var(--accent)}
.opt{display:flex;gap:15px;align-items:flex-start;padding:15px 6px;border-bottom:1px solid var(--line);cursor:pointer;transition:background .2s ease}
.opt:hover{background:rgba(33,28,19,.035)}
.opt input{position:absolute;opacity:0;pointer-events:none}
.opt .dot,.opt .cb{width:15px;height:15px;border:1.5px solid var(--ink);border-radius:50%;flex:none;margin-top:5px;position:relative}
.opt .cb{border-radius:0}
.opt input:checked+.dot::after{content:"";position:absolute;inset:3px;border-radius:50%;background:var(--ink)}
.opt input:checked+.cb::after{content:"";position:absolute;left:4.5px;top:1.5px;width:4px;height:8px;border:solid var(--ink);border-width:0 2px 2px 0;transform:rotate(45deg)}
.opt .ot{flex:1}
.opt .ot b{font-weight:400;font-size:16.5px}
.opt .ot small{display:block;font:italic 13.5px var(--serif);color:var(--faint);margin-top:3px}
.opt input:checked~.ot b{color:var(--accent)}
.opt .op{font:12px var(--mono);white-space:nowrap;margin-top:5px;color:var(--soft)}
.opt input:focus-visible+.dot,.opt input:focus-visible+.cb{outline:2px solid var(--accent);outline-offset:2px}
.opt.dis{opacity:.4;pointer-events:none}
@media(max-width:1000px){.cfg{grid-template-columns:1fr}}
/* ============ dark process ============ */
.dark{background:var(--dark);color:var(--paper)}
.dark .kk{color:rgba(244,240,230,.6)}
.dark .lead{color:rgba(244,240,230,.72)}
.steps{margin-top:clamp(40px,6vw,72px)}
.step{display:grid;grid-template-columns:56px 1fr auto;gap:clamp(16px,3vw,44px);border-top:1px solid var(--pline);padding:clamp(26px,3.4vw,40px) 0}
.step .sn{font:13px var(--mono);color:rgba(244,240,230,.45);padding-top:7px}
.step h3{font-weight:400;font-size:clamp(21px,2.2vw,26px);margin-bottom:10px}
.step p{color:rgba(244,240,230,.72);max-width:58ch}
.step .sw{font:11px var(--mono);letter-spacing:.22em;color:var(--accent);padding-top:9px;white-space:nowrap}
@media(max-width:640px){.step{grid-template-columns:40px 1fr}.step .swk{display:none}.step .sw{grid-column:2}}
/* ============ owners ============ */
.q-main{font:400 clamp(28px,4.4vw,46px)/1.22 var(--serif);letter-spacing:-.01em;max-width:24ch;margin:34px 0 26px}
.attr{font:10.5px var(--mono);letter-spacing:.2em;color:var(--faint);text-transform:uppercase}
.q-side{border-left:1px solid var(--line);padding-left:clamp(20px,3vw,44px);margin:clamp(40px,5vw,70px) 0 0 clamp(0px,12vw,180px);max-width:46ch}
.q-side p{font:italic clamp(19px,2vw,23px)/1.5 var(--serif);margin-bottom:16px}
/* ============ promise + stamp ============ */
.pgrid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,4fr);gap:clamp(30px,5vw,80px);align-items:center}
.pgrid .body p{color:var(--soft);max-width:58ch}
.stampw{display:flex;justify-content:center}
.stamp{width:clamp(150px,16vw,190px);transform:rotate(-7deg)}
.stamp .rot{transform-box:view-box;transform-origin:center;animation:rot 44s linear infinite}
@keyframes rot{to{transform:rotate(360deg)}}
.stamp .cts{font:11px var(--mono);letter-spacing:.22em;fill:var(--ink)}
.stamp .ct{font:italic 42px var(--serif);fill:var(--ink)}
@media(max-width:920px){.pgrid{grid-template-columns:1fr}}
/* ============ visit + radial map ============ */
.vgrid{display:grid;grid-template-columns:minmax(0,6fr) minmax(0,6fr);gap:clamp(30px,5vw,90px);align-items:center;margin-top:44px}
.vgrid .body p{color:var(--soft);max-width:56ch}
.addr{font-size:clamp(20px,2.2vw,26px);line-height:1.35;margin:26px 0 22px;font-style:italic}
.hrs div{display:flex;justify-content:space-between;gap:18px;padding:10px 0;border-bottom:1px solid var(--line);font:12px var(--mono);letter-spacing:.12em}
.hrs span:first-child{color:var(--faint)}
.vct{display:flex;gap:14px;margin-top:28px;flex-wrap:wrap}
.ket{font:italic 14px var(--serif);color:var(--faint);margin-top:18px}
.mtx{font:10.5px var(--mono);fill:rgba(33,28,19,.68)}
.mk{stroke:var(--paper);stroke-width:5px;paint-order:stroke}
.rip{fill:none;stroke:var(--accent);stroke-width:1.5;opacity:0;animation:rip 3.4s ease-out infinite}
.rip.r2{animation-delay:1.7s}
@keyframes rip{0%{r:6;opacity:.7}70%{r:30;opacity:0}100%{r:30;opacity:0}}
@media(max-width:920px){.vgrid{grid-template-columns:1fr}}
/* ============ faq ============ */
.acc{margin-top:40px;max-width:820px}
.acc-i{border-bottom:1px solid var(--line)}
.acc-b{width:100%;display:flex;justify-content:space-between;align-items:center;gap:20px;text-align:left;background:none;border:0;
font:400 clamp(18px,1.8vw,21px) var(--serif);padding:21px 2px;cursor:pointer}
.pl{position:relative;width:15px;height:15px;flex:none}
.pl::before,.pl::after{content:"";position:absolute;background:var(--ink);transition:transform .35s ease}
.pl::before{left:0;top:6.5px;width:15px;height:1.5px}
.pl::after{left:6.5px;top:0;width:1.5px;height:15px}
.acc-i.open .pl::after{transform:rotate(90deg);opacity:0}
.acc-p{max-height:0;overflow:hidden;transition:max-height .5s cubic-bezier(.3,.6,.2,1)}
.acc-p p{color:var(--soft);max-width:62ch;padding:0 40px 24px 2px}
/* ============ footer ============ */
.ft{background:var(--dark);color:var(--paper);padding:clamp(56px,8vw,96px) 0 0;position:relative;overflow:hidden}
.ft .tagl{font:italic clamp(24px,3vw,34px) var(--serif);color:rgba(244,240,230,.9)}
.ft .tagl span{font:11px var(--mono);letter-spacing:.24em;font-style:normal;color:var(--accent);display:block;margin-top:10px}
.fcols{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:clamp(24px,4vw,60px);margin-top:clamp(36px,5vw,64px)}
.fcols h4{font:10px var(--mono);letter-spacing:.24em;color:rgba(244,240,230,.45);text-transform:uppercase;font-weight:400;margin-bottom:16px}
.fcols p,.fcols a{font-size:15px;color:rgba(244,240,230,.78);text-decoration:none;line-height:1.9;display:block}
.fcols a:hover{color:var(--accent)}
.flegal{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid var(--pline);margin-top:clamp(36px,5vw,60px);padding:22px 0;
font:10px var(--mono);letter-spacing:.16em;color:rgba(244,240,230,.42);text-transform:uppercase}
.wmark{font:400 clamp(100px,17.5vw,240px)/.76 var(--serif);color:rgba(244,240,230,.07);letter-spacing:-.02em;text-align:center;transform:translateY(14%);user-select:none}
@media(max-width:820px){.fcols{grid-template-columns:1fr 1fr}}
/* ============ modal ============ */
.mo{position:fixed;inset:0;z-index:150;display:grid;place-items:center;padding:18px;background:rgba(26,21,14,.5);backdrop-filter:blur(3px);
visibility:hidden;opacity:0;transition:opacity .3s ease,visibility 0s .3s}
.mo.open{visibility:visible;opacity:1;transition:opacity .3s ease}
.mp{background:var(--paper);border:1px solid var(--ink);width:min(560px,100%);max-height:88vh;overflow:auto;padding:clamp(24px,4vw,40px);
transform:translateY(14px);transition:transform .35s cubic-bezier(.2,.7,.2,1)}
.mo.open .mp{transform:none}
.mh{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px}
.mx{background:none;border:1px solid var(--line);width:34px;height:34px;cursor:pointer;display:grid;place-items:center;transition:border-color .2s}
.mx:hover{border-color:var(--ink)}
.mp h3{font-weight:400;font-size:clamp(24px,3vw,30px);margin-bottom:12px}
.mb{color:var(--soft);margin-bottom:22px}
.ms{border:1px solid var(--line);padding:16px 18px;margin-bottom:26px}
.ms .bs-t b{font-size:22px}
form .fg{margin-bottom:20px}
form label{display:block;font:10px var(--mono);letter-spacing:.22em;color:var(--faint);margin-bottom:6px}
.fi{width:100%;background:none;border:0;border-bottom:1px solid rgba(33,28,19,.4);font:17px var(--serif);color:var(--ink);padding:9px 2px}
.fi:focus{outline:none;border-color:var(--accent)}
.fe{display:block;font:italic 12.5px var(--serif);color:var(--accent);margin-top:6px;min-height:0}
form .btn{width:100%;justify-content:center;margin-top:6px}
.fnp{font:italic 12.5px var(--serif);color:var(--faint);text-align:center;margin-top:14px}
.mdone{text-align:center;padding:10px 0 4px}
.dref{font:13px var(--mono);letter-spacing:.3em;color:var(--accent);margin-bottom:16px}
.mdone .dt{color:var(--soft);margin-bottom:26px}
.mdone .btn{width:auto;margin:0 auto}
/* ============ bike drawings ============ */
.bike{display:block;width:100%;height:auto;color:var(--ink)}
.bike :is(path,line,circle,rect,polygon){stroke:currentColor;fill:none;stroke-linecap:round;stroke-linejoin:round}
.bike .pf{fill:currentColor;stroke:none}
.bike .bp{fill:var(--paint);stroke:none}
.bike .p{stroke:var(--paint)}
.bike .af{fill:var(--accent);stroke:none}
.bike .ac{stroke:var(--accent)}
.bike .beam{fill:var(--beam);stroke:none}
.bike .hide{display:none}
.bike .st>*{stroke-dasharray:1;stroke-dashoffset:1.04;opacity:0;
transition:stroke-dashoffset 1s cubic-bezier(.3,.6,.25,1) var(--d,0s),opacity .55s ease calc(var(--d,0s) + .3s),stroke .45s ease}
.bike.drawn .st>*{stroke-dashoffset:0;opacity:1}
.bike .dimg{opacity:0;transition:opacity .9s ease 1.6s}
.bike.drawn .dimg{opacity:1}
.bike.drawn .beam{animation:breath 5.5s ease-in-out infinite 1.6s}
@keyframes breath{0%,100%{opacity:.16}50%{opacity:.28}}
.bike .dtx{font:13px var(--mono);letter-spacing:.1em;fill:rgba(33,28,19,.62)}
.bike .dm{stroke:rgba(33,28,19,.5);stroke-width:1.2;fill:none;stroke-linecap:round;stroke-linejoin:round}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation:none!important;transition:none!important}
.bike .st>*{stroke-dashoffset:0;opacity:1}
.bike .dimg{opacity:1}
.rv{opacity:1;transform:none}
}
</style>
</head>
<body>
<div class="annc"><span class="dot" aria-hidden="true"></span>Spring builds now booking — <b>four workshop slots left for April</b> · from €3,890</div>
<header class="hd" id="hd">
<div class="wrap hdw">
<a class="brand" href="#top" aria-label="Lumen Cycles, home">
<svg viewBox="0 0 64 64" aria-hidden="true"><circle cx="32" cy="32" r="22" fill="none" stroke="#211C13" stroke-width="6"/><path d="M32 32 L47 17" stroke="#D64F12" stroke-width="7" stroke-linecap="round"/></svg>
<span class="bn">Lumen</span><span class="bc">CYCLES</span>
</a>
<nav class="nl" aria-label="Main">
<a href="#frame">The frame</a><a href="#configure">Build & price</a><a href="#process">Process</a><a href="#visit">Visit</a><a href="#questions">Questions</a>
</nav>
<button class="btn" data-mode="reserve">Reserve a build</button>
<button class="mtg" id="mtg" aria-expanded="false">Menu</button>
<nav class="mnav" id="mnav" aria-label="Mobile">
<a href="#frame">The frame</a><a href="#configure">Build & price</a><a href="#process">Process</a><a href="#visit">Visit</a><a href="#questions">Questions</a><a href="#configure" style="color:var(--accent)">Reserve a build →</a>
</nav>
</div>
</header>
<main>
<!-- ================= HERO ================= -->
<section class="hero wrap" id="top">
<div class="hgrid">
<div class="htx">
<p class="kk rv">Hand-built cargo bikes · København · est. 2017</p>
<h1 class="rv" style="--i:1">Carry everything.<br><em>Park anywhere.</em></h1>
<p class="lead rv" style="--i:2">Lumen builds made-to-order cargo bikes in a galvanising hall on Copenhagen’s Refshaleøen. One rider, one frame, one builder — from first measurement to first ride, in about twelve weeks.</p>
<div class="cta rv" style="--i:3">
<a class="btn" href="#configure">Build yours
<svg width="13" height="13" viewBox="0 0 14 14" aria-hidden="true"><path d="M7 1v11M2 8l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></a>
<button class="btn ghost" data-mode="test">Book a test ride</button>
</div>
<p class="fine rv" style="--i:4">From €3,890 · delivered across the EU · €200 deposit, refundable</p>
</div>
<aside class="stick rv" style="--i:2" aria-label="Specification summary">
<dl>
<div><dt>Model</dt><dd>Lumen One</dd></div>
<div><dt>Steel</dt><dd>Reynolds 725, brazed</dd></div>
<div><dt>Price</dt><dd>from €3,890</dd></div>
<div><dt>Lead time</dt><dd>± 12 weeks</dd></div>
<div><dt>Warranty</dt><dd>10 years, frame</dd></div>
</dl>
<p>EU delivery · test rides Saturdays at the workshop</p>
</aside>
</div>
<figure class="hero-bike rv" style="--i:4">
<div id="hero-bike" aria-label="The Lumen One cargo bike, line drawing"></div>
<figcaption><span>FIG. 01 — THE LUMEN ONE, LEFT ELEVATION</span><em>Drawn by hand. Built the same way.</em></figcaption>
</figure>
</section>
<div class="mq" aria-hidden="true">
<div class="mq-t"><span id="mqs">HÅNDBYGGET I KØBENHAVN · SEDAN 2017 · LADCYKLER EFTER MÅL · REYNOLDS 725 STEEL · TEN-YEAR FRAME WARRANTY · CARGO BIKES MADE TO ORDER · DELIVERED ACROSS THE EU · </span></div>
</div>
<!-- ================= 01 WORKSHOP ================= -->
<section class="sec wrap" id="workshop">
<div class="mani">
<div>
<p class="kk rv">01 — The workshop</p>
<h2 class="rv" style="--i:1">A cargo bike shouldn’t feel like cargo.</h2>
</div>
<div class="body rv" style="--i:2">
<p>We are eight people in a former galvanising hall on Refshalevej, and we make one bike. Not a range — one frame, made to measure, in five honest colours of powder and one shade of raw steel you can still see the grinding marks through.</p>
<p>Since 2017 we have delivered 412 of them — to school runs, brewery deliveries, photographers with too many tripods, and one family whose Great Dane has his own footrest. Every frame is cut for its rider: your inseam, your commute, your hill — and the hill on the way home.</p>
<div class="press">
POLITIKEN — <i>“Årets ladcykel”</i> (2023) · MONOCLE — <i>“The sensible answer to the SUV”</i> · WALLPAPER* — Design Awards, longlist (2022)
</div>
</div>
</div>
</section>
<div class="wrap stats">
<div class="stat rv"><b data-v="12">0</b><span>weeks, deposit to delivery</span></div>
<div class="stat rv" style="--i:1"><b data-v="412">0</b><span>frames delivered since 2017</span></div>
<div class="stat rv" style="--i:2"><b data-v="180">0</b><span>kg rated load, rider included</span></div>
<div class="stat rv" style="--i:3"><b data-v="10">0</b><span>years on every frame we braze</span></div>
</div>
<!-- ================= 02 FRAME ================= -->
<section class="sec wrap" id="frame">
<p class="kk rv">02 — The frame</p>
<h2 class="rv" style="--i:1">The Lumen One.</h2>
<p class="lead rv" style="--i:2">A long, low spine, with the deck slung between the wheels where the weight belongs — under the axles, not above them. That is why a fully loaded Lumen steers like an empty one.</p>
<div class="fgrid">
<figure class="geo rv" id="geofig" style="--i:2">
<span class="tag">FIG. 02 — GEOMETRY, LEFT ELEVATION</span>
<div id="geo-bike"></div>
<figcaption>ILLUSTRATIVE — IKKE TEGNET I MÅLESTOK · DRAWN IN THE WORKSHOP, JANUARY 2025</figcaption>
</figure>
<dl class="spec rv" style="--i:3">
<div><dt>Tubing</dt><dd>Reynolds 725, butted CroMo</dd></div>
<div><dt>Joints</dt><dd>Brass-brazed, hand-finished fillets</dd></div>
<div><dt>Steering</dt><dd>Ackermann linkage, damped</dd></div>
<div><dt>Wheels</dt><dd>650 b · Schwalbe G-One 2.1″</dd></div>
<div><dt>Deck</dt><dd>Oiled bamboo, 620 × 420 mm</dd></div>
<div><dt>Rated load</dt><dd>90 kg deck · 180 kg total</dd></div>
<div><dt>Weight</dt><dd>From 27.8 kg, size M</dd></div>
<div><dt>Brakes</dt><dd>Magura MT5 · 203 / 180 mm</dd></div>
<div><dt>Standard</dt><dd>Proven beyond EN ISO 4210-2</dd></div>
</dl>
</div>
<p class="pull rv">“The deck rides 560 millimetres above the cobbles. You feel it at every red light, in every harbour crosswind, with eighty kilos of gravel aboard — and it feels like nothing at all.”</p>
</section>
<!-- ================= 03 CONFIGURATOR ================= -->
<section class="sec wrap" id="configure">
<p class="kk rv">03 — Build & price · byg din</p>
<h2 class="rv" style="--i:1">Build yours.</h2>
<p class="intro rv" style="--i:2">Every Lumen starts as a conversation. This is the part you can do alone, at midnight, in your socks.</p>
<div class="cfg">
<div class="cfg-l">
<div class="panel rv" style="--i:2">
<span class="tag">LIVE PREVIEW — LEFT SIDE</span>
<div id="cfg-bike" aria-label="Preview of your Lumen One"></div>
</div>
<div class="bs rv" style="--i:3" aria-live="polite">
<div class="bs-h"><span>Build sheet</span><span id="ref-no"></span></div>
<div id="sheet"></div>
</div>
</div>
<div class="cfg-r rv" style="--i:3">
<div class="gh"><b>01 · Paint</b><span id="pick-paint"></span></div>
<div class="sws" id="swatches" role="group" aria-label="Frame colour"></div>
<div class="gh"><b>02 · Cargo module</b><span id="pick-mod"></span></div>
<div id="mod-rows"></div>
<div class="gh"><b>03 · Drivetrain</b><span id="pick-drive"></span></div>
<div id="drive-rows"></div>
<div class="gh"><b>04 · Extras</b><span id="pick-extras"></span></div>
<div id="extra-rows"></div>
</div>
</div>
</section>
<!-- ================= 04 PROCESS ================= -->
<section class="sec dark" id="process">
<div class="wrap">
<p class="kk rv">04 — The process</p>
<h2 class="rv" style="--i:1">Twelve weeks, give or take.</h2>
<p class="lead rv" style="--i:2">Give or take, because brazing has no undo key. We publish the real wait at the top of this page — and we would rather promise fourteen weeks and deliver in twelve than the other way round.</p>
<div class="steps">
<article class="step rv"><div class="sn">01</div><div class="swk"><h3>The conversation</h3><p>Forty-five minutes at the workshop, or on the phone. Bring the school bags, the dog, the cello — we measure your cargo as carefully as your inseam, then you ride test frames rigged to your numbers.</p></div><div class="sw">WEEK 0</div></article>
<article class="step rv" style="--i:1"><div class="sn">02</div><div class="swk"><h3>The fitting</h3><p>Saddle height, reach, deck layout, colour seen in daylight and under streetlight. You approve the drawing. A €200 deposit makes it real — refundable until the first tube is cut.</p></div><div class="sw">WEEK 1</div></article>
<article class="step rv" style="--i:2"><div class="sn">03</div><div class="swk"><h3>The build</h3><p>Cutting, brazing, aligning, filing, filing again. Powder coat in Kalundborg, then wheels, belt, brakes and box. You will get photographs on brazing day — most people frame them.</p></div><div class="sw">WEEKS 2–10</div></article>
<article class="step rv" style="--i:3"><div class="sn">04</div><div class="swk"><h3>Delivery</h3><p>Handed over fitted, at your door or your street corner, anywhere in the EU. The first service at 500 km is on us. So is the coffee.</p></div><div class="sw">WEEK 12</div></article>
</div>
</div>
</section>
<!-- ================= 05 OWNERS ================= -->
<section class="sec wrap" id="owners">
<p class="kk rv">05 — Owners</p>
<blockquote>
<p class="q-main rv" style="--i:1">“We sold the car in March. Nobody has mentioned it since.”</p>
<p class="attr rv" style="--i:2">Mette Lindqvist — Nørrebro · two kids, one dog, a cello</p>
</blockquote>
<div class="q-side rv" style="--i:2">
<p>“The only bike I’ve owned that gets compliments from strangers at red lights.”</p>
<p class="attr">Jonas Ehrhardt — Vesterbro · delivers beer for a living</p>
</div>
</section>
<!-- ================= 06 PROMISE ================= -->
<section class="sec wrap" id="promise">
<div class="pgrid">
<div>
<p class="kk rv">06 — The promise</p>
<h2 class="rv" style="--i:1">Ten years. Frame and braze.</h2>
<div class="body rv" style="--i:2">
<p>If a Lumen frame ever fails while you own it — fatigue, corrosion, a pothole the size of a bathtub — we repair or replace it, no receipt archaeology, and the warranty follows the bike, not the buyer. Crashes we mend at cost, for as long as we exist; we intend to exist for a very long time.</p>
<p>Everything that wears — belts, pads, bearings — is standard, so any competent shop can keep a Lumen rolling, from Malmö to Marseille.</p>
</div>
</div>
<div class="stampw rv" style="--i:3">
<svg class="stamp" viewBox="0 0 160 160" aria-hidden="true">
<defs>
<path id="arcT" d="M 18 80 A 62 62 0 1 1 142 80"/>
<path id="arcB" d="M 18 80 A 62 62 0 1 0 142 80"/>
</defs>
<circle cx="80" cy="80" r="74" fill="none" stroke="#211C13" stroke-width="2"/>
<circle cx="80" cy="80" r="40" fill="none" stroke="#211C13" stroke-width="1.5"/>
<g class="rot">
<text class="cts"><textPath href="#arcT" textLength="188" lengthAdjust="spacingAndGlyphs">LUMEN CYCLES · KØBENHAVN ·</textPath></text>
<text class="cts"><textPath href="#arcB" textLength="188" lengthAdjust="spacingAndGlyphs">· HÅNDBYGGET · EST. 2017 ·</textPath></text>
</g>
<text class="ct" x="80" y="93" text-anchor="middle">10</text>
<text class="cts" x="80" y="112" text-anchor="middle" style="font-size:8.5px">ÅRS GARANTI</text>
</svg>
</div>
</div>
</section>
<!-- ================= 07 VISIT ================= -->
<section class="sec wrap" id="visit">
<p class="kk rv">07 — Visit · besøg os</p>
<h2 class="rv" style="--i:1">Come ride one.</h2>
<div class="vgrid">
<div class="rv" style="--i:2" id="radial" aria-label="Travel times by cargo bike from the workshop"></div>
<div class="body rv" style="--i:3">
<p>The workshop doubles as the showroom: two frames usually on the stand, one usually on the floor with your name on masking tape. Come see the jig, the brass, the smell of linseed. Saturdays are for test rides — book ahead, because the sandbags go quickly.</p>
<p class="addr">Refshalevej 187A,<br>1432 København K</p>
<div class="hrs">
<div><span>THU — FRI</span><span>10 – 17</span></div>
<div><span>SATURDAY</span><span>10 – 14</span></div>
<div><span>TEST RIDES</span><span>SAT · BOOK AHEAD</span></div>
<div><span>PHONE</span><span><a href="tel:+4532148762" style="color:inherit;text-decoration:none">+45 32 14 87 62</a></span></div>
<div><span>WRITE</span><span><a href="mailto:hej@lumencycles.dk" style="color:inherit;text-decoration:none">hej@lumencycles.dk</a></span></div>
</div>
<div class="vct">
<button class="btn" data-mode="test">Book a test ride</button>
<a class="btn ghost" href="mailto:hej@lumencycles.dk?subject=A%20question%20about%20the%20Lumen%20One">Write to us</a>
</div>
<p class="ket">The kettle is usually on.</p>
</div>
</div>
</section>
<!-- ================= 08 QUESTIONS ================= -->
<section class="sec wrap" id="questions">
<p class="kk rv">08 — Questions</p>
<h2 class="rv" style="--i:1">Fair questions.</h2>
<div class="acc rv" style="--i:2">
<div class="acc-i"><button class="acc-b" aria-expanded="false">How long — honestly?<span class="pl" aria-hidden="true"></span></button>
<div class="acc-p"><p>Twelve weeks, give or take two. Brazing has no undo, and powder coat cures on its own schedule. The current wait is published at the top of this page, and we would rather promise fourteen and deliver twelve than the other way round.</p></div></div>
<div class="acc-i"><button class="acc-b" aria-expanded="false">Do you deliver outside Denmark?<span class="pl" aria-hidden="true"></span></button>
<div class="acc-p"><p>Across the EU, crated and 95 % assembled, typically €140–240 depending on distance. Or ride it home from the workshop — several owners have, one of them from Zurich.</p></div></div>
<div class="acc-i"><button class="acc-b" aria-expanded="false">Can I pay in instalments?<span class="pl" aria-hidden="true"></span></button>
<div class="acc-p"><p>Yes — spread across three to twelve months through most banks, or split in three interest-free parts directly with us. We would rather you rode than saved.</p></div></div>
<div class="acc-i"><button class="acc-b" aria-expanded="false">What if something breaks?<span class="pl" aria-hidden="true"></span></button>
<div class="acc-p"><p>Wearing parts are standard, so any competent bike shop can service a Lumen anywhere. The frame itself is covered for ten years, transferable with the bike. Crashes we repair at cost, for as long as we exist.</p></div></div>
<div class="acc-i"><button class="acc-b" aria-expanded="false">Why no electric assist?<span class="pl" aria-hidden="true"></span></button>
<div class="acc-p"><p>An e-cargo bike is a fine thing — it is just not our thing. Copenhagen is flat, and our frames carry the load low, which does most of what a motor is asked to do. If your route genuinely needs assist, we will say so honestly and point you to someone good.</p></div></div>
</div>
</section>
</main>
<!-- ================= FOOTER ================= -->
<footer class="ft" id="ft">
<div class="wrap">
<p class="tagl">Bygget til at bære.<span>BUILT TO CARRY · CARGO BIKES MADE TO ORDER · KØBENHAVN</span></p>
<div class="fcols">
<div>
<h4>Workshop</h4>
<p>Refshalevej 187A<br>1432 København K<br>Denmark</p>
<p style="margin-top:10px"><a href="mailto:hej@lumencycles.dk">hej@lumencycles.dk</a><br><a href="tel:+4532148762">+45 32 14 87 62</a></p>
</div>
<div><h4>The bike</h4><a href="#frame">The frame</a><a href="#configure">Build & price</a><a href="#workshop">The workshop</a></div>
<div><h4>Buying</h4><a href="#process">Process</a><a href="#visit">Visit & test rides</a><a href="#questions">Questions</a></div>
<div><h4>Small print</h4><p>CVR 39 41 87 62<br>Prices incl. 25 % moms/VAT<br>EN ISO 4210-2 tested<br>10-year frame warranty</p></div>
</div>
<div class="flegal">
<span>© Lumen Cycles ApS 2017–2025</span>
<span>Designed & built in Copenhagen — obviously</span>
</div>
</div>
<div class="wmark" aria-hidden="true">LUMEN</div>
</footer>
<!-- ================= MODAL ================= -->
<div class="mo" id="mo" role="dialog" aria-modal="true" aria-labelledby="mtt">
<div class="mp">
<div class="mh">
<span class="kk" id="mkick">Reserve a build</span>
<button class="mx" id="mx" aria-label="Close">
<svg width="14" height="14" viewBox="0 0 14 14"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.6"/></svg>
</button>
</div>
<h3 id="mtt">Your Lumen One, as specified</h3>
<p class="mb" id="mbl"></p>
<div class="ms" id="msheet"></div>
<form id="mf" novalidate>
<div class="fg"><label for="fn">Name</label><input class="fi" id="fn" autocomplete="name"><em class="fe" id="fe-n"></em></div>
<div class="fg"><label for="fe">Email</label><input class="fi" id="fe" type="email" autocomplete="email"><em class="fe" id="fe-e"></em></div>
<div class="fg"><label for="fp">Phone — optional</label><input class="fi" id="fp" type="tel" autocomplete="tel"></div>
<button class="btn" type="submit" id="msub">Request this build</button>
<p class="fnp" id="mnote"></p>
</form>
<div class="mdone" id="mdone" hidden>
<p class="dref" id="mref"></p>
<p class="dt" id="mdone-t"></p>
<button class="btn ghost" id="mclose2">Close</button>
</div>
</div>
</div>
<div class="grain" aria-hidden="true"></div>
<script>
'use strict';
const $=(s,c=document)=>c.querySelector(s), $$=(s,c=document)=>[...c.querySelectorAll(s)];
const fmt=v=>'€'+v.toLocaleString('en-IE');
/* ============================================================
The bike — one drawing, three uses:
hero (with light on), live configurator preview, geometry chart.
============================================================ */
function bikeSVG(o={}){
const R=n=>(+n).toFixed(1);
const spk=(cx,cy)=>{let s='';for(let i=0;i<12;i++){const a=i*Math.PI/6;
s+=`<line x1="${R(cx+13*Math.cos(a))}" y1="${R(cy+13*Math.sin(a))}" x2="${R(cx+59*Math.cos(a))}" y2="${R(cy+59*Math.sin(a))}" stroke-width="1.4" opacity=".45"/>`}return s};
const core=`
<g class="st hide" data-extra="light" style="--d:1.45s">
<polygon class="beam" points="188,198 188,210 10,352 2,296"/>
<rect x="744" y="318" width="16" height="9" rx="2" stroke-width="2.5"/>
</g>
<g class="st" style="--d:0s">
<circle cx="170" cy="350" r="73" stroke-width="13"/><circle cx="170" cy="350" r="62" stroke-width="3.5"/>
${spk(170,350)}<circle cx="170" cy="350" r="16" stroke-width="3"/><circle cx="170" cy="350" r="10" class="pf"/>
</g>
<g class="st" style="--d:0s">
<circle cx="790" cy="350" r="73" stroke-width="13"/><circle cx="790" cy="350" r="62" stroke-width="3.5"/>
${spk(790,350)}<circle cx="790" cy="350" r="10" class="pf"/>
</g>
<g class="st" style="--d:.25s">
<path d="M 86.3 380.4 A 89 89 0 0 1 253.7 380.4" stroke-width="5.5"/>
<path d="M 706.3 380.4 A 89 89 0 0 1 873.7 380.4" stroke-width="5.5"/>
</g>
<g class="st" style="--d:.35s">
<g data-drive="nexus"><rect x="772" y="341" width="36" height="18" rx="6" stroke-width="3.5"/><circle cx="790" cy="350" r="9" class="pf"/></g>
<g data-drive="rohloff" class="hide"><rect x="760" y="336" width="60" height="28" rx="8" stroke-width="3.5"/><circle cx="790" cy="350" r="9" class="pf"/></g>
<g data-drive="deore" class="hide"><circle cx="790" cy="350" r="19.5" stroke-width="1.6"/><circle cx="790" cy="350" r="15.5" stroke-width="1.6"/><circle cx="790" cy="350" r="11.5" stroke-width="1.6"/><circle cx="790" cy="350" r="7.5" stroke-width="1.6"/><circle cx="790" cy="350" r="8" class="pf"/></g>
<g data-run="belt"><path d="M 551 315 L 786 337" stroke-width="5"/><path d="M 551 372 L 786 362" stroke-width="5"/></g>
<g data-run="chain" class="hide"><path d="M 551 312 L 786 335" stroke-width="2.6"/><path d="M 551 318 L 786 338" stroke-width="2.6"/><path d="M 551 369 L 786 362" stroke-width="2.6"/><path d="M 551 375 L 786 366" stroke-width="2.6"/></g>
</g>
<g class="st" style="--d:.45s">
<path class="p" d="M 243 148 L 224 207" stroke-width="12"/>
<path class="p" d="M 224 207 C 212 258 186 305 170 350" stroke-width="9"/>
<path class="p" d="M 233 193 C 310 244 430 288 542 326" stroke-width="10"/>
<path class="p" d="M 224 207 C 298 262 432 308 546 336" stroke-width="10"/>
<path class="p" d="M 548 344 L 602 218" stroke-width="10"/>
<path class="p" d="M 602 224 C 680 262 740 312 786 349" stroke-width="7"/>
<path class="p" d="M 552 336 C 630 342 730 347 786 350" stroke-width="7"/>
<circle cx="548" cy="344" r="13" class="bp"/>
<circle cx="238" cy="167" r="3.2" class="af"/>
</g>
<g class="st" style="--d:.8s">
<path class="p" d="M 100 238 C 140 231 292 231 326 238 L 326 254 C 292 261 140 261 100 254 Z" stroke-width="6"/>
<path class="p" d="M 114 254 C 148 238 186 222 214 208" stroke-width="9"/>
<path class="p" d="M 318 254 C 358 268 398 278 432 286" stroke-width="9"/>
<g stroke-width="2.4"><line x1="128" y1="234.5" x2="128" y2="258"/><line x1="163" y1="233" x2="163" y2="260"/><line x1="198" y1="232.4" x2="198" y2="261"/><line x1="233" y1="232.4" x2="233" y2="261"/><line x1="268" y1="233" x2="268" y2="260"/><line x1="298" y1="234.5" x2="298" y2="258"/></g>
</g>
<g class="st hide" data-mod="box" style="--d:1s">
<path class="p" d="M 117 238 L 110 154 Q 109 148 115 148 L 311 148 Q 317 148 316 154 L 311 238 Z" stroke-width="6"/>
<g stroke-width="2"><line x1="146" y1="156" x2="146" y2="236"/><line x1="186" y1="156" x2="186" y2="236"/><line x1="226" y1="156" x2="226" y2="236"/><line x1="266" y1="156" x2="266" y2="236"/></g>
<path d="M 111 166 L 315 166" stroke-width="2"/>
</g>
<g class="st hide" data-mod="family" style="--d:1s">
<rect x="128" y="224" width="54" height="16" rx="7" stroke-width="3.5"/><rect x="178" y="180" width="11" height="58" rx="5" stroke-width="3.5"/>
<rect x="222" y="224" width="54" height="16" rx="7" stroke-width="3.5"/><rect x="272" y="180" width="11" height="58" rx="5" stroke-width="3.5"/>
<path class="ac" d="M 136 222 Q 158 205 180 222" stroke-width="3"/><path class="ac" d="M 230 222 Q 252 205 274 222" stroke-width="3"/>
</g>
<g class="st hide" data-extra="rails" style="--d:1.35s">
<path d="M 104 226 L 322 226" stroke-width="3"/><line x1="122" y1="226" x2="122" y2="234" stroke-width="2.5"/><line x1="182" y1="226" x2="182" y2="234" stroke-width="2.5"/><line x1="242" y1="226" x2="242" y2="234" stroke-width="2.5"/><line x1="302" y1="226" x2="302" y2="234" stroke-width="2.5"/>
</g>
<g class="st hide" data-extra="tray" style="--d:1.35s">
<path d="M 200 108 C 224 100 262 100 284 108 L 284 115 C 262 107 224 107 200 115 Z" stroke-width="3"/>
<path d="M 243 115 L 245 146" stroke-width="3"/>
</g>
<g class="st hide" data-extra="lock" style="--d:1.35s">
<rect x="736" y="316" width="24" height="14" rx="3" stroke-width="3"/>
</g>
<g class="st" style="--d:1.1s">
<path class="p" d="M 243 148 C 254 141 262 137 272 134" stroke-width="8"/>
<path d="M 268 132 C 298 118 324 121 340 134 C 352 143 357 156 359 170" stroke-width="7"/>
<path d="M 352 157 C 357 162 359 167 359 171" stroke-width="11"/>
<circle cx="287" cy="127" r="4" stroke-width="2.5"/>
<path d="M 602 218 L 613 197" stroke-width="5"/>
<path class="pf" d="M 574 197 C 578 187 600 181 616 185 C 625 187 627 194 618 198 C 603 203 582 204 574 197 Z"/>
</g>
<g class="st" style="--d:1.2s">
<circle cx="548" cy="344" r="30" stroke-width="5"/>
<line x1="554.6" y1="346.4" x2="570.6" y2="352.2" stroke-width="2.5"/><line x1="545.6" y1="350.6" x2="539.8" y2="362.5" stroke-width="2.5"/><line x1="541.4" y1="341.6" x2="525.4" y2="335.8" stroke-width="2.5"/><line x1="550.4" y1="337.4" x2="556.2" y2="325.5" stroke-width="2.5"/>
<circle cx="548" cy="344" r="5.5" class="pf"/>
<path d="M 548 344 L 566 384" stroke-width="6"/><path d="M 553 386 L 579 386" stroke-width="7"/>
</g>
<g class="st" style="--d:1.35s">
<path d="M 216 195 L 204 201" stroke-width="4"/><circle cx="196" cy="204" r="8" class="pf"/>
</g>`;
const stat=`<path d="M 30 431 H 930" stroke-width="1.6" stroke-dasharray="0 9" opacity=".38"/>`;
const dims=o.dims?`
<g class="dimg">
<g class="dm">
<path d="M 162 350 H 178 M 170 342 V 358"/><path d="M 782 350 H 798 M 790 342 V 358"/>
<path d="M 170 364 V 472" stroke-dasharray="3 5"/><path d="M 790 364 V 472" stroke-dasharray="3 5"/>
<path d="M 90 364 V 512" stroke-dasharray="3 5"/><path d="M 870 364 V 512" stroke-dasharray="3 5"/>
<path d="M 170 466 H 790"/><path d="M 90 506 H 870"/>
<polyline points="180,461 170,466 180,471"/><polyline points="780,461 790,466 780,471"/>
<polyline points="100,501 90,506 100,511"/><polyline points="860,501 870,506 860,511"/>
<path d="M 326 246 H 356" stroke-dasharray="3 5"/><path d="M 336 432 H 356" stroke-dasharray="3 5"/>
<path d="M 352 246 V 432"/><polyline points="347,257 352,247 357,257"/><polyline points="347,421 352,431 357,421"/>
<path d="M 243 148 H 323" stroke-dasharray="3 5"/><path d="M 285 148 A 42 42 0 0 0 256 108"/>
<polyline points="326,240 424,140 690,140"/>
</g>
<text class="dtx" x="480" y="458" text-anchor="middle">WHEELBASE — 1 180 MM</text>
<text class="dtx" x="480" y="498" text-anchor="middle">OVERALL — 2 240 MM</text>
<text class="dtx" x="344" y="342" text-anchor="middle" transform="rotate(-90 344 342)">DECK HEIGHT — 560 MM</text>
<text class="dtx" x="298" y="126">72°</text>
<text class="dtx" x="430" y="132">DECK LOAD 90 KG · TOTAL 180 KG</text>
</g>`:'';
// pathLength normalisation powers the draw-on animation (dims excluded)
const drawn=core.replace(/<(path|circle|line|rect|polygon)\b/g,'<$1 pathLength="1"');
return `<svg class="bike" style="--paint:${o.paint||'#D64F12'}" viewBox="${o.vb||'0 0 960 448'}" role="img" aria-label="${o.title||'The Lumen One cargo bike, left elevation, line drawing'}"><title>${o.title||'The Lumen One — line drawing'}</title>${drawn}${stat}${dims}</svg>`;
}
/* ---- inject the three drawings ---- */
$('#hero-bike').innerHTML=bikeSVG({showLight:true,title:'The Lumen One cargo bike in Signal Orange, with headlight'});
$('#cfg-bike').innerHTML=bikeSVG({title:'Live preview of your configured Lumen One'});
$('#geo-bike').innerHTML=bikeSVG({dims:true,vb:'0 0 960 522',title:'Geometry chart of the Lumen One with dimensions'});
setTimeout(()=>$('#hero-bike .bike').classList.add('drawn'),250);
/* ============================================================
Configurator
============================================================ */
const PAINTS={
signal:{n:'Signal Orange',hex:'#D64F12',d:0,t:'single-coat powder'},
oxide:{n:'Oxide Red',hex:'#6F2F26',d:180,t:'two-coat premium'},
moss:{n:'Moss Green',hex:'#566340',d:0,t:'single-coat powder'},
slate:{n:'Harbour Slate',hex:'#44525C',d:120,t:'two-coat premium'},
raw:{n:'Raw Steel',hex:'#94897A',d:190,t:'hand-rubbed clear coat'},
black:{n:'Black',hex:'#28231C',d:0,t:'matte powder'}
};
const MODULES={
deck:{n:'Flat deck',d:'Bamboo slats, six tie-down points, converts to anything.',p:0},
box:{n:'Weather box',d:'Powder-coated aluminium, draining floor, lockable lid, two benches.',p:540},
family:{n:'Family kit',d:'Two seats with 5-point straps, grab-rail, ready for the rain canopy.',p:760}
};
const DRIVES={
nexus:{n:'Carbon belt · Shimano Nexus 8',s:'BELT · NEXUS 8',d:'Clean, quiet, winter-proof. The sensible heart.',p:0},
rohloff:{n:'Carbon belt · Rohloff Speedhub 14',s:'BELT · ROHLOFF 14',d:'1,400 % gear range. Service interval: never.',p:780},
deore:{n:'Chain · Shimano Deore 11',s:'CHAIN · DEORE 11',d:'For the purists — we credit €210 back.',p:-210}
};
const EXTRAS={
light:{n:'SON 28 dynamo + Busch & Müller lights',d:'Sees and is seen, forever, without charging.',p:290},
lock:{n:'ABUS frame lock',d:'Keyed to match your front door.',p:85},
rails:{n:'Low side rails, bamboo',d:'A 40 mm lip for the wobbly things. Flat deck only.',p:95},
tray:{n:'Bamboo tray for the rider’s bag',d:'Keys, phone, the good coffee.',p:120}
};
const WEIGHT={base:27.8,box:6.4,family:4.1,nexus:0,rohloff:0.7,deore:-0.3,light:0.35,lock:0.4,rails:1.1,tray:0.6};
const BASE=3890, REF='LC-25-'+(120+Math.floor(Math.random()*860));
const state={paint:'signal',module:'deck',drive:'nexus',extras:new Set()};
const cfgSvg=()=>$('#cfg-bike .bike');
/* swatches */
$('#swatches').innerHTML=Object.entries(PAINTS).map(([k,p])=>`
<button type="button" class="swb" data-paint="${k}" aria-pressed="false" title="${p.n} — ${p.t}">
<span class="sw" style="background:${p.hex}"></span>
<span class="snm">${p.n}</span>
${p.d?`<span class="spd">+ ${fmt(p.d)}</span>`:''}
</button>`).join('');
/* radio / checkbox rows */
const rows=(obj,key,type)=>Object.entries(obj).map(([k,v])=>`
<label class="opt">
<input class="vh" type="${type}" name="cfg-${key}" value="${k}">
<span class="${type==='radio'?'dot':'cb'}"></span>
<span class="ot"><b>${v.n}</b><small>${v.d}</small></span>
<span class="op">${v.p===0?'incl.':v.p>0?'+ '+fmt(v.p):'− '+fmt(-v.p)}</span>
</label>`).join('');
$('#mod-rows').innerHTML=rows(MODULES,'mod','radio');
$('#drive-rows').innerHTML=rows(DRIVES,'drive','radio');
$('#extra-rows').innerHTML=rows(EXTRAS,'extras','checkbox');
$('#ref-no').textContent='№ '+REF+' — DRAFT';
function summary(){
const P=PAINTS[state.paint];
const list=[
{l:'Lumen One — made-to-measure frameset',p:BASE},
{l:`Paint — ${P.n} · ${P.t}`,p:P.d,hide0:P.d===0},
{l:`Cargo — ${MODULES[state.module].n}`,p:MODULES[state.module].p},
{l:`Drivetrain — ${DRIVES[state.drive].n}`,p:DRIVES[state.drive].p},
...[...state.extras].map(k=>({l:'Extra — '+EXTRAS[k].n,p:EXTRAS[k].p}))
];
const total=BASE+P.d+MODULES[state.module].p+DRIVES[state.drive].p+[...state.extras].reduce((s,k)=>s+EXTRAS[k].p,0);
const weight=WEIGHT.base+WEIGHT[state.module]+WEIGHT[state.drive]+[...state.extras].reduce((s,k)=>s+(WEIGHT[k]||0),0);
return {list,total,weight};
}
function sheetRows(withTotal){
const {list,total,weight}=summary();
const body=list.map(r=>`<div class="bs-r"><span>${r.l}</span><i class="ld"></i><b>${r.p===0?'incl.':r.p>0?'+ '+fmt(r.p):'− '+fmt(-r.p)}</b></div>`).join('');
if(!withTotal) return body;
return body+`<div class="bs-t"><span>Total, incl. 25 % VAT</span><b id="total">${fmt(total)}</b></div>
<div class="bs-w">≈ ${weight.toFixed(1)} kg build weight · deposit €200 · balance before delivery</div>
<button class="btn" data-mode="reserve">Reserve this build
<svg width="13" height="13" viewBox="0 0 14 14" aria-hidden="true"><path d="M1 7h11M8 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
<p class="bs-n">Nothing is charged on this page. The deposit link arrives after we’ve talked — refundable until the first tube is cut.</p>`;
}
let shownTotal=BASE;
function tweenTotal(v){
const el=$('#total'); if(!el){return}
const from=shownTotal,to=v,t0=performance.now();
const step=t=>{const p=Math.min(1,(t-t0)/450),e=1-Math.pow(1-p,3);
shownTotal=Math.round(from+(to-from)*e); el.textContent=fmt(shownTotal);
if(p<1)requestAnimationFrame(step)};
requestAnimationFrame(step);
}
function render(){
const P=PAINTS[state.paint],M=MODULES[state.module],D=DRIVES[state.drive],svg=cfgSvg();
svg.style.setProperty('--paint',P.hex);
$$('[data-mod]',svg).forEach(g=>g.classList.toggle('hide',g.dataset.mod!==state.module));
$$('[data-drive]',svg).forEach(g=>g.classList.toggle('hide',g.dataset.drive!==state.drive));
$$('[data-run]',svg).forEach(g=>g.classList.toggle('hide',(state.drive==='deore')!==(g.dataset.run==='chain')));
$$('[data-extra]',svg).forEach(g=>g.classList.toggle('hide',!state.extras.has(g.dataset.extra)));
// rails only fit the flat deck
const railsOK=state.module==='deck';
if(!railsOK) state.extras.delete('rails');
$$('#extra-rows input').forEach(i=>{
i.disabled=!railsOK&&i.value==='rails';
i.checked=state.extras.has(i.value);
i.closest('.opt').classList.toggle('dis',i.disabled);
});
$$('#mod-rows input,#drive-rows input').forEach(i=>i.checked=i.value===state.module||i.value===state.drive);
$$('#swatches .swb').forEach(b=>b.setAttribute('aria-pressed',b.dataset.paint===state.paint));
$('#pick-paint').textContent=P.n+(P.d?' · + '+fmt(P.d):' · incl.');
$('#pick-mod').textContent=M.n+(M.p?' · + '+fmt(M.p):' · incl.');
$('#pick-drive').textContent=D.s+(D.p?' · '+(D.p>0?'+ '+fmt(D.p):'− '+fmt(-D.p)):' · incl.');
$('#pick-extras').textContent=state.extras.size?state.extras.size+' selected':'none yet';
$('#sheet').innerHTML=sheetRows(true);
tweenTotal(summary().total);
}
render();
$('#swatches').addEventListener('click',e=>{const b=e.target.closest('.swb'); if(b){state.paint=b.dataset.paint;render()}});
$('#mod-rows').addEventListener('change',e=>{state.module=e.target.value;render()});
$('#drive-rows').addEventListener('change',e=>{state.drive=e.target.value;render()});
$('#extra-rows').addEventListener('change',e=>{
if(e.target.checked) state.extras.add(e.target.value); else state.extras.delete(e.target.value);
render();
});
/* ============================================================
Modal — reserve / test ride
============================================================ */
const mo=$('#mo'),mf=$('#mf'),mdone=$('#mdone'); let mMode='reserve';
function openModal(mode){
mMode=mode;
$('#mkick').textContent=mode==='test'?'Book a test ride':'Reserve a build';
$('#mtt').textContent=mode==='test'?'A Saturday at Refshalevej':'Your Lumen One, as specified';
$('#mbl').textContent=mode==='test'
?'Saturdays we clear a corner of the workshop and set out frames in every size, loaded with sandbags until they feel like your life does. Forty-five minutes. Rain or rain.'
:'The full build sheet is below. We’ll ring or write within two working days to talk it through before anything is binding.';
const sheet=$('#msheet');
if(mode==='test'){sheet.hidden=true}
else{sheet.hidden=false; const {weight}=summary();
sheet.innerHTML=sheetRows(true)+`<div class="bs-w" style="margin-top:10px">Ref ${REF} · ≈ ${weight.toFixed(1)} kg</div>`;
}
$('#msub').textContent=mode==='test'?'Request a Saturday':'Request this build';
$('#mnote').textContent=mode==='test'
?'Nothing is booked until we confirm a time by email.'
:'Nothing is charged on this page. After we’ve talked, we send a link for the €200 deposit.';
mf.hidden=false; mf.reset(); mdone.hidden=true;
$$('.fe').forEach(e=>e.textContent='');
mo.classList.add('open'); document.body.classList.add('lock');
setTimeout(()=>$('#fn').focus(),90);
}
function closeModal(){mo.classList.remove('open');document.body.classList.remove('lock')}
document.addEventListener('click',e=>{
const t=e.target.closest('[data-mode]');
if(t){openModal(t.dataset.mode);}
if(e.target.closest('.btn[data-mode=reserve],.bs .btn')){ /* handled by data-mode */ }
});
$('#mx').addEventListener('click',closeModal);
$('#mclose2').addEventListener('click',closeModal);
mo.addEventListener('click',e=>{if(e.target===mo)closeModal()});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&mo.classList.contains('open'))closeModal()});
mf.addEventListener('submit',e=>{
e.preventDefault();
const n=$('#fn').value.trim(),m=$('#fe').value.trim(); let ok=true;
$('#fe-n').textContent=n?'':'We need a name to shout across the workshop.'; if(!n)ok=false;
$('#fe-e').textContent=/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(m)?'':'That email doesn’t look deliverable.'; if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(m))ok=false;
if(!ok)return;
$('#mref').textContent='REF '+REF+(mMode==='test'?' · SATURDAY':' · BUILD');
$('#mdone-t').textContent=mMode==='test'
?'Tak — you’re on the Saturday list. We’ll confirm a time by email within one working day. Ask for the coffee, not the decaf.'
:'Tak — it’s reserved. We’ll ring or write within two working days to talk it through; ask for Anders, he runs the build list.';
mf.hidden=true; mdone.hidden=false; $('#mref').focus();
});
/* ============================================================
Radial travel-time map (Visit)
============================================================ */
(function(){
const DEST=[['Den Lille Havfrue',3,-12,'e'],['Operaen',7,55,'e'],['Kongens Nytorv',6,115,'w'],
['Nørreport St.',8,205,'w'],['Fælledparken',9,-40,'e'],['Københavns H.',11,165,'w'],
['Assistens',13,245,'n'],['Amager Strand',16,35,'w']];
let s='';
s+=`<circle cx="280" cy="280" r="225" fill="none" stroke="rgba(33,28,19,.28)" stroke-width="1.2" stroke-dasharray="2 6"/>`;
s+=`<circle cx="280" cy="280" r="125" fill="none" stroke="rgba(33,28,19,.28)" stroke-width="1.2" stroke-dasharray="2 6"/>`;
DEST.forEach(([n,min,ang,anc])=>{
const a=ang*Math.PI/180, r=min*12.5;
const x=280+r*Math.cos(a), y=280+r*Math.sin(a);
s+=`<line x1="280" y1="280" x2="${R(x)}" y2="${R(y)}" stroke="rgba(33,28,19,.22)" stroke-width="1"/>`;
s+=`<circle cx="${R(x)}" cy="${R(y)}" r="3.5" fill="#211C13"/>`;
const lx=anc==='w'?x-10:x+10, ty=anc==='n'?y-12:y+4;
s+=`<text class="mtx" x="${R(lx)}" y="${R(ty)}" text-anchor="${anc==='w'?'end':anc==='n'?'middle':'start'}">${n} — ${min}′</text>`;
});
s+=`<circle class="rip" cx="280" cy="280" r="6"/><circle class="rip r2" cx="280" cy="280" r="6"/>`;
s+=`<circle cx="280" cy="280" r="5" fill="#D64F12"/>`;
s+=`<text class="mtx mk" x="280" y="304" text-anchor="middle" font-size="10" letter-spacing="1.5">LUMEN WORKSHOP — REFSHALEØEN</text>`;
s+=`<text class="mtx mk" x="280" y="152" text-anchor="middle">10 MIN — 2.5 KM</text>`;
s+=`<text class="mtx mk" x="280" y="51" text-anchor="middle">20 MIN — 5 KM</text>`;
$('#radial').innerHTML=`<svg viewBox="0 0 560 480" role="img" aria-label="Travel times by cargo bike from the workshop">${s}</svg>`;
function R(n){return (+n).toFixed(1)}
})();
/* ============================================================
Behaviour: reveals, counters, draw-in, nav, accordion
============================================================ */
const io=('IntersectionObserver' in window)
?new IntersectionObserver(es=>es.forEach(en=>{
if(!en.isIntersecting)return;
en.target.classList.add('on');
if(en.target.dataset.v){countUp(en.target)}
io.unobserve(en.target);
if(en.target.id==='configure')$('#cfg-bike .bike').classList.add('drawn');
if(en.target.id==='geofig')$('#geo-bike .bike').classList.add('drawn');
}),{threshold:.18})
:null;
$$('.rv').forEach(el=>io?io.observe(el):el.classList.add('on'));
if(!io){$('#cfg-bike .bike').classList.add('drawn');$('#geo-bike .bike').classList.add('drawn')}
$$('[data-v]').forEach(el=>io?io.observe(el):countUp(el));
function countUp(el){
const v=+el.dataset.v,t0=performance.now(),dur=1200;
const step=t=>{const p=Math.min(1,(t-t0)/dur),e=1-Math.pow(1-p,3);
el.textContent=Math.round(v*e).toLocaleString('en-IE');
if(p<1)requestAnimationFrame(step)};
requestAnimationFrame(step);
}
/* header + mobile menu */
const hd=$('#hd');
addEventListener('scroll',()=>hd.classList.toggle('sc',scrollY>12),{passive:true});
const mtg=$('#mtg'),mnav=$('#mnav');
mtg.addEventListener('click',()=>{
const open=mnav.classList.toggle('open');
mtg.setAttribute('aria-expanded',open); mtg.textContent=open?'Close':'Menu';
});
mnav.addEventListener('click',e=>{if(e.target.tagName==='A'){mnav.classList.remove('open');mtg.textContent='Menu'}});
/* accordion */
$$('.acc-i').forEach((it,i)=>{
const b=$('.acc-b',it),p=$('.acc-p',it);
b.addEventListener('click',()=>{
const open=it.classList.toggle('open');
b.setAttribute('aria-expanded',open);
p.style.maxHeight=open?p.scrollHeight+'px':'0px';
});
if(i===0){it.classList.add('open');b.setAttribute('aria-expanded','true');p.style.maxHeight=p.scrollHeight+'px'}
});
/* marquee: duplicate the strip so the loop is seamless */
$('#mqs').parentElement.innerHTML=`<span id="mqs">${$('#mqs').textContent}</span>`.repeat(1)+`<span>${$('#mqs').textContent}</span>`;
</script>
</body>
</html>

