Run details
- Model
- z-ai/glm-5.3-flash
- Route
- OpenRouter API, one user message
- Served by
- OpenInference
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 131,072 output tokens (the model's limit)
- Date
- 31 Oct 2026
- Time
- 22 min 43 s
- Size
- 76 kB
- Tokens
- 113 in, 98,011 out (71,774 of them thinking)
- List price
- $0.014 at today's price ($0.016 in / $0.148 out per M)
- Paid
- $0.003
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
76 kB 1,299 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 — Custom cargo bikes, built to order in Copenhagen</title>
<meta name="description" content="Lumen Cycles designs, brazes and paints custom cargo bikes to order in Copenhagen. Three frames, one workshop, a six-to-eight-week build.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4' fill='%23D9541E'/%3E%3Cg stroke='%23D9541E' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.2 2.2M16.9 16.9l2.2 2.2M19.1 4.9l-2.2 2.2M7.1 16.9l-2.2 2.2'/%3E%3C/g%3E%3C/svg%3E">
<style>
/* ---------- foundations ---------- */
:root{
--paper:#F3EDE2; --paper2:#EAE2D1; --ink:#211D16; --ink2:#4B4536; --muted:#8B7F6C;
--line:rgba(33,29,22,.16); --accent:#D9541E; --accent-deep:#B5430F;
--dark:#171310; --dark-line:rgba(241,234,218,.18); --dark-text:#F1EAD9; --dark-mut:rgba(241,234,218,.68);
--serif:"Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,Georgia,serif;
--mono:ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,"Liberation Mono",monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth;scroll-padding-top:96px;-webkit-text-size-adjust:100%}
body{background:var(--paper);color:var(--ink2);font:400 17px/1.68 var(--serif);-webkit-font-smoothing:antialiased}
body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:250;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.045'/%3E%3C/svg%3E")}
::selection{background:var(--accent);color:var(--paper)}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit}
svg{display:block}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.wrap{max-width:1260px;margin:0 auto;padding-inline:clamp(20px,4vw,48px)}
.mono{font-family:var(--mono)}
h1,h2,h3{font-family:var(--serif);color:var(--ink);font-weight:400;letter-spacing:-.01em}
/* kickers & headings */
.kicker{display:flex;align-items:center;gap:10px;font:600 11px/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--ink);margin-bottom:24px}
.kicker .lamp-ic{width:15px;height:15px;color:var(--accent);flex:none}
h2{font-size:clamp(2rem,3.8vw,3.2rem);line-height:1.08;max-width:22ch}
.lead{margin-top:18px;max-width:58ch;font-size:1.05rem}
.sec{padding:clamp(76px,9vw,124px) 0}
.sec-head{margin-bottom:clamp(42px,6vw,68px)}
/* buttons & links */
.btn{display:inline-flex;align-items:center;gap:10px;font:600 12px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
padding:17px 28px;background:var(--ink);color:var(--paper);border:1px solid var(--ink);cursor:pointer;
transition:background .18s,color .18s,border-color .18s,transform .18s}
.btn:hover{background:var(--accent);border-color:var(--accent);color:#161008;transform:translateY(-1px)}
.btn-sm{padding:12px 20px}
.btn-accent{background:var(--accent);border-color:var(--accent);color:#161008}
.btn-accent:hover{background:var(--dark-text);border-color:var(--dark-text);color:var(--dark)}
.dark .btn-accent{background:var(--accent);color:#161008}
.dark .btn-accent:hover{background:var(--dark-text);color:var(--dark)}
.link-arrow{display:inline-flex;align-items:center;gap:9px;font:600 12px/1 var(--mono);letter-spacing:.15em;text-transform:uppercase;
color:var(--ink);background:none;border:0;cursor:pointer;padding:4px 0}
.link-arrow .arr{transition:transform .2s}
.link-arrow:hover{color:var(--accent)}
.link-arrow:hover .arr{transform:translateX(5px)}
/* reveal on scroll */
.js .reveal{opacity:0;transform:translateY(20px);transition:opacity .7s ease,transform .7s ease;transition-delay:var(--d,0s)}
.js .reveal.in{opacity:1;transform:none}
/* ---------- header ---------- */
.site-head{position:fixed;top:0;left:0;right:0;z-index:60;transition:background .3s,box-shadow .3s}
.site-head.scrolled{background:rgba(243,237,226,.93);backdrop-filter:blur(10px);box-shadow:0 1px 0 var(--line)}
.site-head .bar{display:flex;align-items:center;justify-content:space-between;gap:20px;max-width:1360px;margin:0 auto;padding:16px clamp(20px,4vw,44px)}
.brand{display:flex;align-items:center;gap:11px}
.brand .lamp-ic{width:21px;height:21px;color:var(--accent);transition:transform .5s cubic-bezier(.6,0,.2,1)}
.brand:hover .lamp-ic{transform:rotate(45deg)}
.brand-name{font:600 1.3rem/1 var(--serif);color:var(--ink);letter-spacing:-.01em}
.brand-name small{font:600 9.5px var(--mono);letter-spacing:.3em;color:var(--muted);display:block;margin-top:3px}
.site-nav{display:flex;gap:clamp(18px,2.6vw,36px);align-items:center}
.site-nav a{font:600 11.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--ink2);padding:6px 0;position:relative}
.site-nav a::after{content:"";position:absolute;left:0;bottom:0;height:1.5px;width:100%;background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform .25s}
.site-nav a:hover{color:var(--ink)}
.site-nav a:hover::after,.site-nav a.cur::after{transform:scaleX(1)}
.site-nav a.cur{color:var(--ink)}
.head-cta{display:inline-flex}
.menu-btn{display:none;width:44px;height:44px;background:none;border:1px solid var(--line);cursor:pointer;position:relative}
.menu-btn span{position:absolute;left:12px;right:12px;height:1.6px;background:var(--ink);transition:.25s}
.menu-btn span:nth-child(1){top:17px}.menu-btn span:nth-child(2){top:26px}
.menu-btn[aria-expanded="true"] span:nth-child(1){top:21px;transform:rotate(45deg)}
.menu-btn[aria-expanded="true"] span:nth-child(2){top:21px;transform:rotate(-45deg)}
.mnav{position:fixed;inset:0;z-index:70;background:var(--paper);display:flex;flex-direction:column;padding:22px clamp(20px,6vw,40px) 40px;
transform:translateY(-102%);transition:transform .5s cubic-bezier(.7,0,.2,1)}
.mnav.open{transform:none}
.mnav-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:8vh}
.mnav-close{width:44px;height:44px;background:none;border:1px solid var(--line);cursor:pointer;font-family:var(--mono);font-size:16px}
.mnav a{font:400 clamp(1.9rem,7vw,2.6rem)/1.25 var(--serif);color:var(--ink);padding:12px 0;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline}
.mnav a small{font:500 10px var(--mono);letter-spacing:.2em;color:var(--muted)}
.mnav-foot{margin-top:auto;font:500 12px/2 var(--mono);letter-spacing:.08em;color:var(--ink2)}
/* ---------- hero ---------- */
.hero{padding:clamp(130px,16vh,180px) 0 clamp(40px,6vh,70px)}
.hero-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(36px,5vw,76px);align-items:center}
.hero h1{font-size:clamp(2.7rem,5.4vw,4.9rem);line-height:1.04;margin-bottom:26px}
.hero h1 em{font-style:italic;color:var(--accent)}
.hero .sub{max-width:46ch;margin-bottom:34px}
.cta-row{display:flex;align-items:center;gap:28px;flex-wrap:wrap}
.slot{display:flex;align-items:center;gap:10px;margin-top:26px;font:500 11px/1 var(--mono);letter-spacing:.18em;color:var(--ink2)}
.slot::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--accent);animation:blink 2.6s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
.hero-copy>*{opacity:0}
.js .hero-copy>*{animation:rise .8s cubic-bezier(.3,.6,.2,1) both}
.js .hero-copy>*:nth-child(2){animation-delay:.12s}
.js .hero-copy>*:nth-child(3){animation-delay:.24s}
.js .hero-copy>*:nth-child(4){animation-delay:.36s}
.js .hero-copy>*:nth-child(5){animation-delay:.48s}
@keyframes rise{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:none}}
/* drawing sheets */
.sheet{--frame:var(--accent);--box:#E5C893;position:relative;border:1px solid var(--line);background:var(--paper);
background-image:radial-gradient(rgba(33,29,22,.13) 1px,transparent 1.35px);background-size:22px 22px}
.sheet::after{content:"";position:absolute;inset:9px;border:1px dashed rgba(33,29,22,.22);pointer-events:none}
.corner-note{position:absolute;font:500 9.5px/1 var(--mono);letter-spacing:.18em;color:var(--muted);z-index:2}
.corner-note.tl{top:16px;left:18px}
.corner-note.br{bottom:16px;right:18px}
.sheet svg{width:100%;height:auto;position:relative;z-index:1}
.fig{margin-top:14px;font:500 10.5px/1.6 var(--mono);letter-spacing:.16em;color:var(--muted);text-transform:uppercase}
/* bikes (shared drawing styles) */
.bike text{font-family:var(--mono);font-size:13.5px;letter-spacing:.06em;fill:var(--ink)}
.bike .tube{stroke:var(--frame);stroke-width:7;stroke-linecap:round;stroke-linejoin:round;fill:none;transition:stroke .25s}
.bike .thin{stroke-width:5}
.bike .ink{stroke:var(--ink);fill:none;stroke-linecap:round;stroke-linejoin:round;transition:stroke .25s}
.bike .tire{stroke-width:10}
.bike .rim{stroke-width:2.5;opacity:.85}
.bike .spoke{stroke-width:1.4;opacity:.55}
.bike .hub{fill:var(--ink);transition:fill .25s}
.bike .chain{stroke-width:2.4;opacity:.9}
.bike.belted .chain{stroke-width:5.5;opacity:1}
.bike .saddle{fill:var(--ink)}
.bike .boxy{fill:var(--box);stroke:var(--ink);stroke-width:3.5;transition:fill .25s,stroke .25s}
.bike .slat{stroke:var(--ink);stroke-width:1.5;opacity:.45}
.bike .boxtext{font-size:15px;letter-spacing:.24em}
.bike .lamp{fill:var(--accent)}
.bike .ray{stroke:var(--accent);stroke-width:2.4;stroke-linecap:round;animation:flicker 7s infinite}
@keyframes flicker{0%,90%,100%{opacity:1}93%{opacity:.25}96%{opacity:.85}}
.bike .shadow{fill:rgba(33,29,22,.09)}
.bike .ground{stroke:var(--ink);stroke-width:1.6;stroke-dasharray:12 9;opacity:.35}
.bike .canopy{stroke:var(--accent);stroke-width:4.5;stroke-linecap:round;fill:var(--accent);fill-opacity:.09}
.bike .far{opacity:.32}
.bike .callout{cursor:default}
.bike .callout polyline{stroke:var(--ink);stroke-width:1.3;fill:none;opacity:.7;transition:.2s}
.bike .callout circle{fill:var(--accent)}
.bike .callout text{font-size:13.5px;letter-spacing:.08em;fill:var(--ink);transition:fill .2s}
.bike .callout.hot text{fill:var(--accent)}
.bike .callout.hot polyline{stroke:var(--accent);opacity:1}
.bike [data-part].hot :is(.tube,.ink,.boxy){stroke:var(--accent)}
.bike [data-part].hot .hub{fill:var(--accent)}
@media (max-width:760px){.callout{display:none}}
/* ---------- ticker ---------- */
.ticker{overflow:hidden;border-block:1px solid var(--line);background:var(--paper)}
.ticker-track{display:flex;width:max-content;animation:tick 52s linear infinite}
.ticker:hover .ticker-track{animation-play-state:paused}
.ticker-seg{white-space:nowrap;font:600 11.5px/1 var(--mono);letter-spacing:.22em;text-transform:uppercase;color:var(--ink);padding:15px 0}
.ticker-seg i{font-style:normal;color:var(--accent);padding:0 26px}
@keyframes tick{to{transform:translateX(-50%)}}
/* ---------- machines ---------- */
.mach-grid{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,8fr);gap:clamp(30px,4vw,64px);align-items:start}
.model-nav{position:sticky;top:108px}
.model-tab{display:grid;grid-template-columns:auto 1fr;column-gap:18px;text-align:left;width:100%;padding:24px 16px 24px 8px;
background:none;border:0;border-bottom:1px solid var(--line);border-left:3px solid transparent;cursor:pointer;transition:.2s}
.model-tab .mn-no{grid-row:1/3;align-self:center;font:500 11px var(--mono);letter-spacing:.14em;color:var(--muted);transition:color .2s}
.model-tab .mn-name{font:400 clamp(1.7rem,2.4vw,2.15rem)/1.1 var(--serif);color:var(--ink);transition:color .2s}
.model-tab .mn-sub{font:500 10.5px var(--mono);letter-spacing:.16em;color:var(--muted);margin-top:8px}
.model-tab:hover .mn-name{color:var(--accent)}
.model-tab.on{border-left-color:var(--accent);background:rgba(33,29,22,.035)}
.model-tab.on .mn-no{color:var(--accent)}
.machine-main .sheet{margin-bottom:0}
.machine-lower{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);gap:clamp(28px,4vw,54px);margin-top:clamp(28px,3.5vw,46px)}
#machineBody{transition:opacity .26s,transform .26s}
#machineBody.swap{opacity:0;transform:translateY(8px)}
.specs{border-bottom:1px solid var(--line)}
.spec{display:flex;justify-content:space-between;gap:16px;padding:13px 2px;border-top:1px solid var(--line);
font:500 12px/1.4 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.spec b{color:var(--ink);font-weight:600;text-align:right}
.machine-copy h3{font-size:clamp(1.5rem,2.2vw,1.9rem)}
.machine-copy h3 span{font-style:italic;color:var(--muted)}
.machine-copy p{margin:14px 0 24px;max-width:52ch}
/* ---------- configurator (dark) ---------- */
.dark{background:var(--dark);color:var(--dark-mut)}
.dark .kicker,.dark h2{color:var(--dark-text)}
.dark .lead{color:var(--dark-mut)}
.cfg-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(30px,4.5vw,72px);align-items:start}
.opt-label{font:600 10.5px/1 var(--mono);letter-spacing:.22em;text-transform:uppercase;color:var(--dark-mut);margin-bottom:14px}
.opt-label b{color:var(--dark-text);font-weight:600}
.cfg-group{margin-bottom:34px}
.segs{display:flex;flex-wrap:wrap;gap:10px}
.segs button{display:inline-flex;align-items:center;gap:10px;border:1px solid var(--dark-line);background:none;color:var(--dark-mut);
font:600 11px/1 var(--mono);letter-spacing:.14em;padding:12px 16px;cursor:pointer;transition:.18s}
.segs button i{width:13px;height:13px;border-radius:2px;display:inline-block;border:1px solid rgba(0,0,0,.25)}
.segs button:hover{border-color:var(--dark-text);color:var(--dark-text)}
.segs button.on{border-color:var(--dark-text);background:rgba(241,234,218,.08);color:var(--dark-text)}
.swatches{display:flex;align-items:center;gap:13px;flex-wrap:wrap}
.swatch{width:34px;height:34px;border-radius:50%;border:1px solid rgba(241,234,218,.35);cursor:pointer;transition:transform .15s;padding:0}
.swatch:hover{transform:scale(1.14)}
.swatch.on{box-shadow:0 0 0 3px var(--dark),0 0 0 4.5px var(--dark-text)}
.paint-name{font:500 11px var(--mono);letter-spacing:.16em;color:var(--dark-text);text-transform:uppercase;margin-left:4px}
.chk-list{border-top:1px solid var(--dark-line)}
.chk{display:flex;align-items:center;gap:14px;padding:13px 2px;border-bottom:1px solid var(--dark-line);cursor:pointer;position:relative}
.chk input{position:absolute;opacity:0;pointer-events:none}
.chk .box{width:18px;height:18px;border:1.5px solid var(--dark-text);flex:none;position:relative;transition:.18s}
.chk .box::after{content:"";position:absolute;left:4.5px;top:1px;width:5px;height:9.5px;border-right:2.2px solid #171310;border-bottom:2.2px solid #171310;transform:rotate(45deg);opacity:0}
.chk input:checked ~ .box{background:var(--accent);border-color:var(--accent)}
.chk input:checked ~ .box::after{opacity:1}
.chk input:focus-visible ~ .box{outline:2px solid var(--accent);outline-offset:2px}
.chk-n{font:400 15px var(--serif);color:var(--dark-text);flex:1}
.chk-p{font:500 11px var(--mono);letter-spacing:.1em;color:var(--dark-mut)}
.dark .sheet{border-color:rgba(241,234,218,.14);box-shadow:0 28px 60px -22px rgba(0,0,0,.6)}
.price-box{margin-top:6px;border-top:1px solid var(--dark-line);padding-top:26px;display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:22px}
.price-big{font:600 clamp(2.1rem,3.4vw,2.9rem)/1 var(--mono);color:var(--dark-text);letter-spacing:-.01em}
.price-sub{font:500 10.5px var(--mono);letter-spacing:.18em;color:var(--dark-mut);margin-top:10px;text-transform:uppercase}
.serial{margin-top:20px;font:500 10.5px/1.8 var(--mono);letter-spacing:.16em;color:var(--dark-mut);text-transform:uppercase}
.serial b{color:var(--accent);font-weight:600}
/* ---------- process ---------- */
.proc-grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,4fr);gap:clamp(36px,6vw,96px);align-items:start}
.steps{list-style:none;border-top:1px solid var(--line)}
.step{display:grid;grid-template-columns:96px 1fr auto;gap:26px;padding:30px 8px;border-bottom:1px solid var(--line);position:relative}
.step::after{content:"";position:absolute;left:0;top:-1px;height:1px;width:100%;background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform .5s cubic-bezier(.6,0,.2,1)}
.step:hover::after{transform:scaleX(1)}
.step-no{font:600 13px var(--mono);letter-spacing:.18em;color:var(--muted);padding-top:7px;transition:color .25s}
.step:hover .step-no{color:var(--accent)}
.step h3{font-size:1.45rem;margin-bottom:7px}
.step p{max-width:56ch}
.step-meta{font:500 10.5px var(--mono);letter-spacing:.16em;color:var(--muted);padding-top:9px;white-space:nowrap}
.proc-aside{position:sticky;top:120px;border-left:2px solid var(--accent);padding-left:clamp(20px,2.5vw,34px)}
.proc-aside blockquote{font:italic 400 1.45rem/1.45 var(--serif);color:var(--ink)}
.proc-aside figcaption{margin-top:18px;font:500 10.5px/1.8 var(--mono);letter-spacing:.18em;color:var(--muted);text-transform:uppercase}
.proc-aside .warranty{margin-top:30px;padding-top:22px;border-top:1px solid var(--line);font-size:.95rem}
/* ---------- voices ---------- */
.voices{background:var(--paper2);border-block:1px solid var(--line)}
.q-wrap{max-width:880px;margin:0 auto;position:relative;padding:0 clamp(0px,4vw,40px)}
.qmark{position:absolute;top:-30px;left:-4px;font:italic 400 8rem/1 var(--serif);color:var(--accent);user-select:none;pointer-events:none}
.q-box{transition:opacity .26s,transform .26s}
.q-box.fade{opacity:0;transform:translateY(10px)}
.q-box blockquote{font:italic 400 clamp(1.45rem,3.2vw,2.3rem)/1.42 var(--serif);color:var(--ink);letter-spacing:-.01em}
.q-attr{margin-top:26px;font:500 11.5px/1.6 var(--mono);letter-spacing:.18em;color:var(--ink2);text-transform:uppercase}
.q-controls{display:flex;align-items:center;gap:14px;margin-top:40px}
.q-btn{width:46px;height:46px;display:grid;place-items:center;background:none;border:1px solid var(--ink);cursor:pointer;color:var(--ink);transition:.18s}
.q-btn svg{width:16px;height:16px}
.q-btn:hover{background:var(--ink);color:var(--paper)}
.q-idx{margin-left:auto;font:500 11.5px var(--mono);letter-spacing:.2em;color:var(--muted)}
/* ---------- visit ---------- */
.visit-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(32px,5vw,72px);align-items:start}
.addr{font-size:clamp(1.35rem,2vw,1.7rem);line-height:1.35;color:var(--ink)}
.visit-hours{margin:28px 0;border-bottom:1px solid var(--line)}
.visit-note{margin:26px 0 30px;max-width:44ch;padding:18px 20px;border:1px solid var(--line);background:var(--paper2);font-size:.95rem}
.contact-line{display:flex;flex-wrap:wrap;gap:8px 28px;margin-bottom:30px}
.contact-line a{font:600 12px/1 var(--mono);letter-spacing:.12em;color:var(--ink);border-bottom:1px solid var(--accent);padding-bottom:3px;transition:color .2s}
.contact-line a:hover{color:var(--accent)}
.map-panel{border:1px solid var(--line);background:var(--paper2)}
.map{width:100%;height:auto}
.map-serif{font:italic 400 17px var(--serif);fill:#6D7C77}
.map-mono{font-family:var(--mono);font-size:10px;letter-spacing:.28em;fill:var(--ink2)}
.map-mono.big{font-size:12px;letter-spacing:.32em;fill:var(--muted)}
.map .halo{transform-box:fill-box;transform-origin:center;animation:halo 2.8s ease-out infinite}
@keyframes halo{0%{transform:scale(.5);opacity:.9}80%{transform:scale(3.4);opacity:0}100%{opacity:0}}
/* ---------- footer ---------- */
.site-foot{background:var(--dark);color:var(--dark-mut);padding:clamp(64px,8vw,100px) 0 34px}
.foot-top{display:flex;flex-wrap:wrap;align-items:baseline;gap:14px 40px;margin-bottom:clamp(44px,6vw,72px)}
.foot-mark{display:flex;align-items:center;gap:14px;font:italic 400 clamp(2.2rem,5vw,3.6rem)/1 var(--serif);color:var(--dark-text)}
.foot-mark .lamp-ic{width:30px;height:30px;color:var(--accent);transition:transform .5s cubic-bezier(.6,0,.2,1)}
.foot-mark:hover .lamp-ic{transform:rotate(45deg)}
.foot-tag{font:italic 400 1.05rem/1.5 var(--serif);max-width:34ch}
.foot-grid{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) minmax(0,1fr);gap:clamp(28px,4vw,60px);padding-bottom:clamp(40px,5vw,64px)}
.foot-grid h4{font:600 10.5px/1 var(--mono);letter-spacing:.24em;text-transform:uppercase;color:var(--accent);margin-bottom:18px}
.foot-grid p,.foot-grid a{font:400 15px/2 var(--serif);color:var(--dark-mut)}
.foot-grid a{display:block;width:fit-content;border-bottom:1px solid transparent;transition:.2s}
.foot-grid a:hover{color:var(--dark-text);border-bottom-color:var(--accent)}
.foot-base{border-top:1px solid var(--dark-line);padding-top:24px;display:flex;flex-wrap:wrap;gap:8px 36px;
font:500 10.5px/1.8 var(--mono);letter-spacing:.12em;color:rgba(241,234,218,.55);text-transform:uppercase}
/* ---------- modal & toast ---------- */
.overlay{position:fixed;inset:0;z-index:120;background:rgba(18,14,9,.58);display:grid;place-items:center;padding:20px;opacity:0;transition:opacity .26s}
.overlay.open{opacity:1}
.overlay[hidden]{display:none}
.modal{background:var(--paper);width:min(540px,100%);max-height:88vh;overflow:auto;border:1px solid var(--ink);
padding:clamp(26px,5vw,42px);position:relative;transform:translateY(16px);transition:transform .26s}
.overlay.open .modal{transform:none}
.modal-x{position:absolute;top:16px;right:16px;width:40px;height:40px;display:grid;place-items:center;background:none;border:1px solid var(--line);cursor:pointer;transition:.18s}
.modal-x:hover{background:var(--ink);color:var(--paper)}
.modal-x svg{width:14px;height:14px}
.modal h3{font-size:clamp(1.6rem,3vw,2.1rem);margin:8px 0 22px}
.m-summary{border:1px dashed rgba(33,29,22,.35);padding:14px 16px;font:500 11px/1.9 var(--mono);letter-spacing:.12em;color:var(--ink2);text-transform:uppercase;margin-bottom:26px}
.field{display:block;margin-bottom:18px}
.f-label{display:flex;justify-content:space-between;align-items:baseline;font:600 10px/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin-bottom:8px}
.f-label .opt{font-weight:500;letter-spacing:.12em}
.field input,.field textarea{width:100%;background:#FAF6EC;border:1px solid rgba(33,29,22,.35);padding:13px 14px;
font:400 16px/1.5 var(--serif);color:var(--ink);border-radius:0;transition:border-color .18s}
.field textarea{min-height:110px;resize:vertical}
.field input:focus,.field textarea:focus{outline:none;border-color:var(--accent)}
.err{display:none;font:500 10.5px var(--mono);letter-spacing:.1em;color:var(--accent);margin-top:7px;text-transform:uppercase}
.field.bad .err{display:block}
.field.bad input{border-color:var(--accent)}
.form-note{margin-top:16px;font:500 10px/1.6 var(--mono);letter-spacing:.14em;color:var(--muted);text-transform:uppercase}
.m-success{text-align:center;padding:26px 6px 8px}
.m-success .lamp-ic{width:44px;height:44px;color:var(--accent);margin:0 auto 20px}
.m-success h3{margin-bottom:12px}
.m-success p{max-width:38ch;margin:0 auto 26px}
.toast{position:fixed;left:50%;bottom:28px;transform:translate(-50%,18px);z-index:140;display:flex;align-items:center;gap:12px;
background:var(--ink);color:var(--paper);font:500 11.5px/1.5 var(--mono);letter-spacing:.1em;padding:15px 22px;opacity:0;transition:.3s;pointer-events:none}
.toast.show{opacity:1;transform:translate(-50%,0)}
.toast .lamp-ic{width:15px;height:15px;color:var(--accent);flex:none}
/* ---------- responsive ---------- */
@media (max-width:1020px){
.hero-grid,.mach-grid,.cfg-grid,.proc-grid,.visit-grid{grid-template-columns:1fr}
.hero{padding-top:120px}
.model-nav{position:static;display:flex;overflow-x:auto;border-block:1px solid var(--line);margin-bottom:34px}
.model-tab{border-bottom:0;white-space:nowrap;padding:18px 22px;min-width:max-content}
.machine-lower{grid-template-columns:1fr}
.proc-aside{position:static;margin-top:44px}
.foot-grid{grid-template-columns:1fr 1fr}
}
@media (max-width:860px){
.site-nav,.head-cta{display:none}
.menu-btn{display:block}
}
@media (max-width:640px){
.step{grid-template-columns:52px 1fr;gap:16px}
.step-meta{grid-column:2;padding-top:0;margin-top:6px}
.foot-grid{grid-template-columns:1fr}
.price-box{flex-direction:column;align-items:stretch}
.price-box .btn{justify-content:center}
}
@media (prefers-reduced-motion:reduce){
html{scroll-behavior:auto}
.ticker-track,.bike .ray,.map .halo,.slot::before{animation:none}
.js .reveal,.js .hero-copy>*{opacity:1;transform:none;animation:none;transition:none}
}
</style>
</head>
<body>
<!-- ======= header ======= -->
<header class="site-head" id="siteHead">
<div class="bar">
<a class="brand" href="#top" aria-label="Lumen Cycles — home">
<svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>
<span class="brand-name">Lumen<small>CYCLES · KBH</small></span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="#machines">Machines</a>
<a href="#build">Build</a>
<a href="#process">Process</a>
<a href="#visit">Visit</a>
</nav>
<button class="btn btn-sm head-cta" type="button" data-open="visit">Book a visit</button>
<button class="menu-btn" type="button" id="menuBtn" aria-expanded="false" aria-controls="mnav" aria-label="Menu"><span></span><span></span></button>
</div>
</header>
<div class="mnav" id="mnav" aria-hidden="true">
<div class="mnav-top">
<span class="brand-name">Lumen<small>CYCLES · KBH</small></span>
<button class="mnav-close" id="mnavClose" aria-label="Close menu">✕</button>
</div>
<a href="#machines">Machines <small>01</small></a>
<a href="#build">Build <small>02</small></a>
<a href="#process">Process <small>03</small></a>
<a href="#visit">Visit <small>05</small></a>
<p class="mnav-foot">Refshalevej 163A · 1432 København K<br>hello@lumencycles.dk · +45 31 62 88 40</p>
</div>
<main id="top">
<!-- ======= hero ======= -->
<section class="hero">
<div class="wrap hero-grid">
<div class="hero-copy">
<p class="kicker"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>Custom cargo bikes · est. 2016 · København</p>
<h1>A bike drawn around <em>your days</em>.</h1>
<p class="sub">Lumen builds cargo bikes to order in an old shipyard hall on Refshaleøen. Geometry, box and paint are decided with you — then brazed by hand and ridden 200 km through Copenhagen before handover.</p>
<div class="cta-row">
<a class="btn" href="#build">Configure your build</a>
<a class="link-arrow" href="#visit">Book a workshop visit <span class="arr">→</span></a>
</div>
<p class="slot">NEXT FRAME SLOT · EARLY MARCH 2026</p>
</div>
<div class="hero-stage">
<div class="sheet">
<span class="corner-note tl mono">LUMEN CYCLES · KØBENHAVN</span>
<span class="corner-note br mono">REF. L-2024-117 · REV. C</span>
<div id="heroStage"></div>
</div>
<p class="fig">FIG. 00 — FRAKT · FRONT LOADER · DRAWN AT THE WORKSHOP, SCALE 1:8 · HOVER THE NOTES</p>
</div>
</div>
</section>
<!-- ======= ticker ======= -->
<div class="ticker" aria-hidden="true">
<div class="ticker-track">
<span class="ticker-seg">Hand-brazed CroMo frames<i>·</i>Built to order — no stock bikes<i>·</i>6–8 weeks, one bike at a time<i>·</i>Up to 340 kg gross load<i>·</i>8-year frame warranty<i>·</i>200 km tested before handover<i>·</i>Bosch Cargo Line mid-drive<i>·</i></span>
<span class="ticker-seg">Hand-brazed CroMo frames<i>·</i>Built to order — no stock bikes<i>·</i>6–8 weeks, one bike at a time<i>·</i>Up to 340 kg gross load<i>·</i>8-year frame warranty<i>·</i>200 km tested before handover<i>·</i>Bosch Cargo Line mid-drive<i>·</i></span>
</div>
</div>
<!-- ======= machines ======= -->
<section class="sec" id="machines">
<div class="wrap">
<header class="sec-head reveal">
<p class="kicker"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>01 · The machines</p>
<h2>Three frames. Every one of them yours.</h2>
<p class="lead">We keep no stock. Each bike starts as a clean sheet in the workshop — these are the three starting points, and the versions people build from them.</p>
</header>
<div class="mach-grid">
<div class="model-nav reveal" role="tablist" aria-label="Models" id="modelNav">
<button class="model-tab" type="button" role="tab" data-model="lang" aria-selected="false">
<span class="mn-no">N°1</span><span class="mn-name">Lang</span><span class="mn-sub">LONGTAIL · FROM €3.900</span>
</button>
<button class="model-tab" type="button" role="tab" data-model="frakt" aria-selected="false">
<span class="mn-no">N°2</span><span class="mn-name">Frakt</span><span class="mn-sub">FRONT LOADER · FROM €4.400</span>
</button>
<button class="model-tab" type="button" role="tab" data-model="vogn" aria-selected="false">
<span class="mn-no">N°3</span><span class="mn-name">Vogn</span><span class="mn-sub">CARGO TRIKE · FROM €4.900</span>
</button>
</div>
<div class="machine-main reveal" style="--d:.1s">
<div class="sheet">
<span class="corner-note tl mono">LUMEN CYCLES · KØBENHAVN</span>
<span class="corner-note br mono" id="mCorner">SHEET 01 / 03</span>
<div id="mStage"></div>
</div>
<p class="fig" id="mFig">FIG. 01 — LANG · LONGTAIL · SCALE 1:8</p>
<div id="machineBody">
<div class="machine-lower">
<div class="specs" id="mSpecs"></div>
<div class="machine-copy">
<h3 id="mTitle">Lang <span>— the everyday longtail.</span></h3>
<p id="mDesc"></p>
<button class="link-arrow" type="button" id="cfgLink" data-config="lang">Configure this model <span class="arr">→</span></button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ======= configurator ======= -->
<section class="sec dark" id="build">
<div class="wrap">
<header class="sec-head reveal">
<p class="kicker"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>02 · Compose yours</p>
<h2>Draw yours on paper first.</h2>
<p class="lead">Pick a frame, a colour and the details. The drawing updates as you go — and the configuration travels with your request. The price you see is the price we honour.</p>
</header>
<div class="cfg-grid">
<div class="cfg-controls reveal">
<div class="cfg-group">
<p class="opt-label">Frame</p>
<div class="segs" id="cModels">
<button type="button" data-model="lang">LANG · LONGTAIL</button>
<button type="button" data-model="frakt" class="on">FRAKT · FRONT LOADER</button>
<button type="button" data-model="vogn">VOGN · TRIKE</button>
</div>
</div>
<div class="cfg-group">
<p class="opt-label">Paint — <b id="paintName">Signal Amber</b></p>
<div class="swatches" id="cfgPaint">
<button type="button" class="swatch on" style="background:#D9541E" aria-label="Signal Amber"></button>
<button type="button" class="swatch" style="background:#26211A" aria-label="Ink Black"></button>
<button type="button" class="swatch" style="background:#5C6B52" aria-label="Moss Green"></button>
<button type="button" class="swatch" style="background:#8A3B2C" aria-label="Oxide Red"></button>
<button type="button" class="swatch" style="background:#B9BCB4" aria-label="Fog Grey"></button>
<span class="paint-name" id="paintNameTag">Signal Amber</span>
</div>
</div>
<div class="cfg-group">
<p class="opt-label">Box / deck</p>
<div class="segs" id="cfgWood">
<button type="button" data-wood="0" class="on"><i style="background:#E5C993"></i>BIRCH PLY</button>
<button type="button" data-wood="1"><i style="background:#8B6A4E"></i>SMOKED OAK</button>
<button type="button" data-wood="2"><i style="background:#BFC3BE"></i>ALUMINIUM</button>
</div>
</div>
<div class="cfg-group">
<p class="opt-label">Details</p>
<div class="chk-list" id="cfgExtras">
<label class="chk"><input type="checkbox" value="belt"><span class="box"></span><span class="chk-n">Gates carbon belt drive</span><span class="chk-p">+ €390</span></label>
<label class="chk"><input type="checkbox" value="canopy"><span class="box"></span><span class="chk-n">Rain canopy, sailcloth</span><span class="chk-p">+ €240</span></label>
<label class="chk"><input type="checkbox" value="battery"><span class="box"></span><span class="chk-n">Second battery, 500 Wh</span><span class="chk-p">+ €420</span></label>
<label class="chk"><input type="checkbox" value="kids"><span class="box"></span><span class="chk-n">Twin child seats + rails</span><span class="chk-p">+ €190</span></label>
</div>
</div>
</div>
<div class="reveal" style="--d:.12s">
<div class="sheet">
<span class="corner-note tl mono">LUMEN CYCLES · KØBENHAVN</span>
<span class="corner-note br mono">SHEET 04 · REV. C — YOURS</span>
<div id="cStage"></div>
</div>
<div class="price-box">
<div>
<p class="opt-label" style="margin-bottom:8px">Estimated build price</p>
<p class="price-big" id="cPrice">€4.400</p>
<p class="price-sub" id="cSummary">FRONT LOADER · SIGNAL AMBER · BIRCH PLY</p>
</div>
<button class="btn btn-accent" type="button" id="reqBtn">Request this build</button>
</div>
<p class="serial">Your build will be <b>N° 248</b> — the drawing sheet is already started.<br>Final quote follows your fitting session · VAT included · EU shipping quoted separately.</p>
</div>
</div>
</div>
</section>
<!-- ======= process ======= -->
<section class="sec" id="process">
<div class="wrap">
<header class="sec-head reveal">
<p class="kicker"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>03 · How a build happens</p>
<h2>Six weeks, five conversations.</h2>
<p class="lead">Nothing here is automated. Every step involves a person at Lumen and a decision that is yours.</p>
</header>
<div class="proc-grid">
<ol class="steps reveal">
<li class="step"><span class="step-no">01</span>
<div><h3>The conversation</h3><p>Coffee at the workshop, or a video call. We map your routes, your cargo and your cellar stairs before anything is drawn.</p></div>
<span class="step-meta">45 MIN</span></li>
<li class="step"><span class="step-no">02</span>
<div><h3>The fitting</h3><p>Measurements, saddle height, test laps on our mule bikes. Your geometry gets drafted by hand — not pulled from a folder.</p></div>
<span class="step-meta">ONE AFTERNOON</span></li>
<li class="step"><span class="step-no">03</span>
<div><h3>Brazing & paint</h3><p>Fillet-brazed 4130 CroMo, cleaned, primered and powder-coated in the colour you chose. You're welcome to watch the torch work.</p></div>
<span class="step-meta">WEEK 3–4</span></li>
<li class="step"><span class="step-no">04</span>
<div><h3>Assembly & the shakedown</h3><p>Wheels built by hand, belt tensioned, brakes bedded in. Then we ride your bike 200 km through Copenhagen before you ever see it.</p></div>
<span class="step-meta">WEEK 5–6</span></li>
<li class="step"><span class="step-no">05</span>
<div><h3>The handover ride</h3><p>You leave with your bike, a fitted helmet and twenty minutes on how to care for it. First year of servicing is on us.</p></div>
<span class="step-meta">ONE HOUR</span></li>
</ol>
<aside class="proc-aside reveal" style="--d:.12s">
<figure>
<blockquote>"A cargo bike is furniture you ride. It should be built like furniture — to measure, by hand, to last decades."</blockquote>
<figcaption>— Rasmus Kyed<br>Founder & frame builder</figcaption>
</figure>
<p class="warranty">Every Lumen frame carries an <strong>8-year warranty</strong>, transferable when you sell the bike — which, in our experience, almost nobody does.</p>
</aside>
</div>
</div>
</section>
<!-- ======= voices ======= -->
<section class="sec voices" id="voices">
<div class="wrap">
<header class="sec-head reveal" style="text-align:center">
<p class="kicker" style="justify-content:center"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>04 · From the saddle</p>
<h2 style="margin-inline:auto">Copenhagen has opinions.</h2>
</header>
<div class="q-wrap reveal">
<span class="qmark" aria-hidden="true">“</span>
<div class="q-box" id="quoteBox">
<blockquote id="qText">“Two kids, a week of groceries and a ladder. It climbs the bridge without me noticing any of it.”</blockquote>
<p class="q-attr" id="qAttr">Mette F. · Nørrebro · Lumen Lang, 2021</p>
</div>
<div class="q-controls">
<button class="q-btn" type="button" id="qPrev" aria-label="Previous story"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 5l-7 7 7 7"/></svg></button>
<button class="q-btn" type="button" id="qNext" aria-label="Next story"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5l7 7-7 7"/></svg></button>
<span class="q-idx" id="qIdx">01 / 04</span>
</div>
</div>
</div>
</section>
<!-- ======= visit ======= -->
<section class="sec" id="visit">
<div class="wrap visit-grid">
<div class="reveal">
<p class="kicker"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>05 · The workshop</p>
<h2>Come and sit on one.</h2>
<p class="lead">The kettle is usually on. Lift a frame, argue about colours, take a mule bike for a lap around the harbour.</p>
<p class="addr" style="margin-top:30px">Refshalevej 163A<br>1432 København K</p>
<div class="visit-hours">
<div class="spec"><span>Tue – Fri</span><b>10:00 – 17:30</b></div>
<div class="spec"><span>Saturday</span><b>10:00 – 14:00</b></div>
<div class="spec"><span>Sun – Mon</span><b>Closed · by appointment</b></div>
</div>
<div class="contact-line">
<a href="mailto:hello@lumencycles.dk">HELLO@LUMENCYCLES.DK</a>
<a href="tel:+4531628840">+45 31 62 88 40</a>
</div>
<button class="btn" type="button" id="visitBook">Book a visit</button>
<p class="visit-note">Good to know: we keep four mule bikes for test rides. If cargo is the point, bring the cargo — a child seat, the week's shopping, whatever you actually carry.</p>
</div>
<div class="map-panel reveal" style="--d:.12s">
<svg class="map" viewBox="0 0 640 460" role="img" aria-label="Hand-drawn map of Refshaleøen, Copenhagen, with the Lumen workshop marked">
<rect width="640" height="460" fill="#EAE2D1"/>
<path d="M0 0 H640 V152 C560 178 486 148 400 170 C310 194 226 158 140 184 C88 199 40 188 0 202 Z" fill="#C9D3CF"/>
<g stroke="#211D16" stroke-width="2" opacity=".4" stroke-linecap="round">
<path d="M152 186 L144 136 M166 184 L160 138"/>
<path d="M332 170 L324 120 M348 168 L342 122"/>
<path d="M512 158 L504 108 M528 156 L522 110"/>
</g>
<g stroke="#5F6E69" stroke-width="1.4" opacity=".55" stroke-linecap="round">
<path d="M70 58 h36 M138 92 h28 M262 52 h30 M428 66 h26 M486 118 h24 M566 60 h28 M330 92 h22"/>
</g>
<text x="46" y="94" class="map-serif">Copenhagen Harbour</text>
<g stroke="#211D16" fill="none" opacity=".5">
<path d="M30 300 H612" stroke-width="1.8"/>
<path d="M30 378 H612" stroke-width="1.2"/>
<path d="M132 262 V440 M298 258 V440 M466 256 V440" stroke-width="1.2"/>
</g>
<g fill="none" stroke="#211D16" opacity=".22">
<rect x="152" y="316" width="118" height="44"/>
<rect x="322" y="314" width="118" height="46"/>
<rect x="490" y="316" width="98" height="44"/>
<rect x="160" y="394" width="110" height="34"/>
<rect x="330" y="394" width="120" height="34"/>
</g>
<text x="176" y="284" class="map-mono big">REFSHALEVEJ</text>
<text x="486" y="440" class="map-mono big">AMAGER →</text>
<text x="60" y="436" class="map-mono">← CITY CENTRE</text>
<g>
<circle class="halo" cx="382" cy="336" r="7" fill="none" stroke="#D9541E" stroke-width="2"/>
<circle cx="382" cy="336" r="6" fill="#D9541E"/>
<path d="M382 344 V382" stroke="#211D16" stroke-width="1.4" opacity=".6"/>
<text x="382" y="398" text-anchor="middle" class="map-mono" style="font-size:11px;letter-spacing:.22em">LUMEN CYCLES</text>
</g>
<g transform="translate(586,58)" stroke="#211D16" fill="none" opacity=".6">
<circle r="15" stroke-width="1.2"/>
<path d="M0 8 V-8 M-4 -3 L0 -9 L4 -3" stroke-width="1.4"/>
</g>
<text x="586" y="34" text-anchor="middle" class="map-mono">N</text>
<g transform="translate(40,430)" stroke="#211D16" opacity=".6">
<path d="M0 0 H70" stroke-width="1.4"/>
<path d="M0 -4 V4 M70 -4 V4" stroke-width="1.4"/>
</g>
<text x="40" y="422" class="map-mono" style="letter-spacing:.18em">250 M</text>
</svg>
</div>
</div>
</section>
</main>
<!-- ======= footer ======= -->
<footer class="site-foot">
<div class="wrap">
<div class="foot-top">
<p class="foot-mark"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>Lumen Cycles</p>
<p class="foot-tag">Custom cargo bikes, brazed and built to order in Copenhagen since 2016.</p>
</div>
<div class="foot-grid">
<div>
<h4>Workshop</h4>
<p>Refshalevej 163A<br>1432 København K<br>Tue–Fri 10:00–17:30 · Sat 10:00–14:00</p>
</div>
<div>
<h4>Pages</h4>
<a href="#machines">The machines</a>
<a href="#build">Compose yours</a>
<a href="#process">How a build happens</a>
<a href="#visit">Visit us</a>
</div>
<div>
<h4>Contact</h4>
<a href="mailto:hello@lumencycles.dk">hello@lumencycles.dk</a>
<a href="tel:+4531628840">+45 31 62 88 40</a>
<button class="link-arrow" type="button" data-open="visit" style="color:var(--dark-text)">Book a visit <span class="arr">→</span></button>
</div>
</div>
<div class="foot-base">
<span>© <span id="year">2025</span> Lumen Cycles ApS · CVR 41 22 87 03</span>
<span>Prices include Danish VAT (25 %)</span>
<span>Built by hand on Refshaleøen</span>
</div>
</div>
</footer>
<!-- ======= modal ======= -->
<div class="overlay" id="overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
<button class="modal-x" type="button" id="mClose" aria-label="Close dialog">
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M1 1l12 12M13 1L1 13"/></svg>
</button>
<p class="kicker" style="margin-bottom:6px" id="mKicker">BUILD REQUEST</p>
<h3 id="mTitle">Request this build</h3>
<p class="m-summary" id="mSummary"></p>
<form id="mForm" novalidate>
<label class="field"><span class="f-label">Name</span>
<input id="fName" name="name" autocomplete="name">
<span class="err">Please add your name.</span></label>
<label class="field"><span class="f-label">Email</span>
<input id="fEmail" name="email" type="email" autocomplete="email">
<span class="err">That email doesn't look right.</span></label>
<label class="field"><span class="f-label">Phone <span class="opt">OPTIONAL</span></span>
<input id="fPhone" name="phone" type="tel" autocomplete="tel"></label>
<label class="field"><span class="f-label">Message</span>
<textarea id="fMsg" name="message"></textarea></label>
<button class="btn btn-accent" type="submit" id="mSubmit" style="width:100%;justify-content:center">Send request</button>
<p class="form-note">We reply within two working days · from hello@lumencycles.dk</p>
</form>
<div class="m-success" id="mSuccess" hidden>
<svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>
<h3 id="sTitle">Request received.</h3>
<p id="sText"></p>
<button class="btn" type="button" id="sClose">Close</button>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite">
<svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>
<span id="toastMsg"></span>
</div>
<!-- ======= bike drawing templates ======= -->
<template id="t-frakt">
<svg class="bike" viewBox="0 0 960 620" role="img" aria-label="Technical side drawing of the Lumen Frakt front-loader cargo bike">
<ellipse class="shadow" cx="690" cy="523" rx="106" ry="7" fill="rgba(33,29,22,.09)"/>
<ellipse class="shadow" cx="215" cy="523" rx="72" ry="6" fill="rgba(33,29,22,.09)"/>
<ellipse class="shadow" cx="556" cy="524" rx="28" ry="4" fill="rgba(33,29,22,.09)"/>
<path class="ink" d="M30 521 H 930" stroke-dasharray="12 9" opacity=".35"/>
<g class="wheel part" data-part="rear" data-cx="690" data-cy="415" data-r="105" data-spokes="14">
<circle class="ink tire d" cx="690" cy="415" r="105" stroke-width="10"/>
<circle class="ink rim d" cx="690" cy="415" r="92" stroke-width="2.5" opacity=".85"/>
<circle class="hub f" cx="690" cy="415" r="7"/>
</g>
<g class="wheel part" data-part="front" data-cx="215" data-cy="448" data-r="72" data-spokes="12">
<circle class="ink tire d" cx="215" cy="448" r="72" stroke-width="9"/>
<circle class="ink rim d" cx="215" cy="448" r="62" stroke-width="2.2" opacity=".85"/>
<circle class="hub f" cx="215" cy="448" r="6"/>
</g>
<g class="drive">
<circle class="ink ring d" cx="540" cy="420" r="26" stroke-width="4"/>
<path class="ink d" d="M540 420 L574 452" stroke-width="5"/>
<path class="ink d" d="M560 452 H594" stroke-width="7"/>
<path class="ink d" d="M540 420 L506 388" stroke-width="5"/>
<path class="ink d" d="M492 388 H526" stroke-width="7"/>
<circle class="ink cog d" cx="690" cy="415" r="13" stroke-width="3"/>
<path class="ink chain d" d="M548 396 L691 403" stroke-width="2.4"/>
<path class="ink chain d" d="M533 443 L688 428" stroke-width="2.4"/>
</g>
<path class="canopy" style="display:none" d="M238 268 Q 379 176 520 268 Z"/>
<g class="frame part" data-part="frame">
<path class="tube d" d="M540 420 L690 415"/>
<path class="tube d" d="M540 420 L378 312"/>
<path class="tube d" d="M380 315 L356 248" stroke-width="8"/>
<path class="tube d" d="M563 278 L358 250"/>
<path class="tube d" d="M540 420 L566 262"/>
<path class="tube d" d="M690 415 L561 290"/>
<path class="tube d" d="M380 315 L215 448"/>
<path class="tube d" d="M356 248 L334 190"/>
<path class="tube d" d="M304 196 Q 334 168 362 186" stroke-width="6"/>
<path class="tube d" d="M256 384 H 506" stroke-width="5"/>
<path class="tube d" d="M260 386 L234 430" stroke-width="4.5"/>
<path class="tube d" d="M500 386 L513 402" stroke-width="4.5"/>
<path class="ink d" d="M540 424 L554 514" stroke-width="4.5"/>
<path class="ink d" d="M548 518 H570" stroke-width="4.5"/>
<path class="ink d" d="M566 262 L572 238" stroke-width="5"/>
</g>
<g class="box part" data-part="box">
<rect class="boxy f" x="250" y="272" width="258" height="108" rx="10"/>
<path class="slat f" d="M292 279 V373 M330 279 V373 M438 279 V373 M476 279 V373"/>
<text class="boxtext f" x="384" y="341" text-anchor="middle" font-size="15" letter-spacing="3">LUMEN · KBH</text>
</g>
<path class="saddle f" d="M540 236 Q 566 226 596 234 Q 566 244 540 236 Z"/>
<g class="lampg">
<rect class="lamp f" x="328" y="232" width="17" height="13" rx="4"/>
<path class="ray d" d="M326 234 L310 229"/>
<path class="ray d" d="M326 238 L308 238"/>
<path class="ray d" d="M326 242 L310 247"/>
</g>
</svg>
</template>
<template id="t-lang">
<svg class="bike" viewBox="0 0 960 620" role="img" aria-label="Technical side drawing of the Lumen Lang longtail cargo bike">
<ellipse class="shadow" cx="698" cy="523" rx="113" ry="7" fill="rgba(33,29,22,.09)"/>
<ellipse class="shadow" cx="200" cy="523" rx="89" ry="6" fill="rgba(33,29,22,.09)"/>
<ellipse class="shadow" cx="462" cy="524" rx="26" ry="4" fill="rgba(33,29,22,.09)"/>
<path class="ink" d="M30 521 H 930" stroke-dasharray="12 9" opacity=".35"/>
<g class="wheel part" data-part="rear" data-cx="698" data-cy="408" data-r="112" data-spokes="14">
<circle class="ink tire d" cx="698" cy="408" r="112" stroke-width="10"/>
<circle class="ink rim d" cx="698" cy="408" r="99" stroke-width="2.5" opacity=".85"/>
<circle class="hub f" cx="698" cy="408" r="7"/>
</g>
<g class="wheel part" data-part="front" data-cx="200" data-cy="432" data-r="88" data-spokes="12">
<circle class="ink tire d" cx="200" cy="432" r="88" stroke-width="10"/>
<circle class="ink rim d" cx="200" cy="432" r="78" stroke-width="2.5" opacity=".85"/>
<circle class="hub f" cx="200" cy="432" r="6"/>
</g>
<g class="drive">
<circle class="ink ring d" cx="450" cy="420" r="26" stroke-width="4"/>
<path class="ink d" d="M450 420 L484 452" stroke-width="5"/>
<path class="ink d" d="M470 452 H504" stroke-width="7"/>
<path class="ink d" d="M450 420 L416 388" stroke-width="5"/>
<path class="ink d" d="M402 388 H436" stroke-width="7"/>
<circle class="ink cog d" cx="698" cy="408" r="13" stroke-width="3"/>
<path class="ink chain d" d="M458 395 L699 396" stroke-width="2.4"/>
<path class="ink chain d" d="M443 444 L697 421" stroke-width="2.4"/>
</g>
<path class="canopy" style="display:none" d="M560 288 Q 614 150 668 290 Z"/>
<g class="frame part" data-part="frame">
<path class="tube d" d="M450 420 L698 408"/>
<path class="tube d" d="M450 420 L292 330"/>
<path class="tube d" d="M296 334 L274 266" stroke-width="8"/>
<path class="tube d" d="M477 272 L278 268"/>
<path class="tube d" d="M450 420 L480 256"/>
<path class="tube d" d="M698 408 L472 296"/>
<path class="tube d" d="M296 332 L202 430"/>
<path class="tube d" d="M274 266 L254 194"/>
<path class="tube d" d="M224 200 Q 254 172 282 188" stroke-width="6"/>
<path class="ink d" d="M450 424 L464 512" stroke-width="4.5"/>
<path class="ink d" d="M456 516 H478" stroke-width="4.5"/>
<path class="ink d" d="M480 256 L483 236" stroke-width="5"/>
<path class="ink d" d="M498 282 L842 296" stroke-width="11"/>
<path class="deck d" d="M501 282 L839 296" stroke-width="5"/>
<path class="ink d" d="M474 300 L498 286" stroke-width="4.5"/>
<path class="ink d" d="M838 296 L745 428" stroke-width="4.5"/>
</g>
<g class="kidseat" style="display:none">
<path class="ink d" d="M592 287 L664 291" stroke-width="7"/>
<path class="ink d" d="M662 289 L668 232" stroke-width="5"/>
<circle class="hub f" cx="668" cy="228" r="4.5"/>
<path class="ink d" d="M592 290 L589 307 M634 291 L631 308" stroke-width="4"/>
</g>
<path class="saddle f" d="M458 232 Q 486 222 516 232 Q 487 242 458 232 Z"/>
<g class="lampg">
<rect class="lamp f" x="246" y="250" width="17" height="13" rx="4"/>
<path class="ray d" d="M244 253 L228 248"/>
<path class="ray d" d="M244 257 L226 257"/>
<path class="ray d" d="M244 261 L228 266"/>
</g>
</svg>
</template>
<template id="t-vogn">
<svg class="bike" viewBox="0 0 960 620" role="img" aria-label="Technical side drawing of the Lumen Vogn cargo trike">
<ellipse class="shadow" cx="720" cy="523" rx="111" ry="7" fill="rgba(33,29,22,.09)"/>
<ellipse class="shadow" cx="205" cy="523" rx="76" ry="6" fill="rgba(33,29,22,.09)"/>
<ellipse class="shadow" cx="558" cy="524" rx="26" ry="4" fill="rgba(33,29,22,.09)"/>
<path class="ink" d="M30 521 H 930" stroke-dasharray="12 9" opacity=".35"/>
<g class="far" opacity=".32">
<circle class="ink tire d" cx="248" cy="442" r="75" stroke-width="9"/>
<circle class="ink rim d" cx="248" cy="442" r="66" stroke-width="2" opacity=".85"/>
<circle class="hub f" cx="248" cy="442" r="5"/>
<path class="tube d" d="M368 308 L250 440" stroke-width="5"/>
</g>
<g class="wheel part" data-part="rear" data-cx="720" data-cy="410" data-r="110" data-spokes="14">
<circle class="ink tire d" cx="720" cy="410" r="110" stroke-width="10"/>
<circle class="ink rim d" cx="720" cy="410" r="97" stroke-width="2.5" opacity=".85"/>
<circle class="hub f" cx="720" cy="410" r="7"/>
</g>
<g class="wheel part" data-part="front" data-cx="205" data-cy="445" data-r="75" data-spokes="12">
<circle class="ink tire d" cx="205" cy="445" r="75" stroke-width="9"/>
<circle class="ink rim d" cx="205" cy="445" r="66" stroke-width="2.2" opacity=".85"/>
<circle class="hub f" cx="205" cy="445" r="5.5"/>
</g>
<g class="drive">
<circle class="ink ring d" cx="540" cy="430" r="26" stroke-width="4"/>
<path class="ink d" d="M540 430 L574 462" stroke-width="5"/>
<path class="ink d" d="M560 462 H594" stroke-width="7"/>
<path class="ink d" d="M540 430 L506 398" stroke-width="5"/>
<path class="ink d" d="M492 398 H526" stroke-width="7"/>
<circle class="ink cog d" cx="720" cy="410" r="13" stroke-width="3"/>
<path class="ink chain d" d="M548 405 L721 412" stroke-width="2.4"/>
<path class="ink chain d" d="M533 453 L719 432" stroke-width="2.4"/>
</g>
<path class="canopy" style="display:none" d="M240 264 Q 382 158 524 264 Z"/>
<g class="frame part" data-part="frame">
<path class="tube d" d="M540 430 L720 410"/>
<path class="tube d" d="M540 430 L570 276"/>
<path class="tube d" d="M565 300 L352 262"/>
<path class="tube d" d="M540 430 L368 318"/>
<path class="tube d" d="M374 314 L350 240" stroke-width="8"/>
<path class="tube d" d="M372 314 L207 443"/>
<path class="tube d" d="M350 240 L330 184"/>
<path class="tube d" d="M300 192 Q 330 164 358 180" stroke-width="6"/>
<path class="tube d" d="M258 384 H 506" stroke-width="5"/>
<path class="tube d" d="M262 386 L237 424" stroke-width="4.5"/>
<path class="tube d" d="M500 386 L513 410" stroke-width="4.5"/>
<path class="ink d" d="M567 276 L560 256" stroke-width="5"/>
</g>
<g class="box part" data-part="box">
<rect class="boxy f" x="252" y="268" width="260" height="112" rx="10"/>
<path class="slat f" d="M296 275 V373 M336 275 V373 M448 275 V373 M488 275 V373"/>
<text class="boxtext f" x="392" y="339" text-anchor="middle" font-size="15" letter-spacing="3">LUMEN · KBH</text>
</g>
<path class="saddle f" d="M516 250 Q 546 240 578 250 Q 547 260 516 250 Z"/>
<g class="lampg">
<rect class="lamp f" x="320" y="228" width="17" height="13" rx="4"/>
<path class="ray d" d="M318 231 L302 226"/>
<path class="ray d" d="M318 235 L300 235"/>
<path class="ray d" d="M318 239 L302 244"/>
</g>
</svg>
</template>
<script>
(() => {
'use strict';
document.documentElement.classList.add('js');
const $ = (s, r=document) => r.querySelector(s);
const $$ = (s, r=document) => Array.from(r.querySelectorAll(s));
const NS = 'http://www.w3.org/2000/svg';
const RM = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const money = n => '€' + n.toLocaleString('da-DK');
$('#year').textContent = new Date().getFullYear();
/* ---------- catalogue data ---------- */
const MODELS = {
lang: { no:'N°1', name:'Lang', type:'Longtail', tag:'the everyday longtail', tpl:'t-lang', base:3900,
fig:'FIG. 01 — LANG · LONGTAIL · SCALE 1:8', sheet:'SHEET 01 / 03',
desc:'A normal bike stretched twenty-five centimetres, so it still rides like a bike — not a van. Two child seats, six bags of shopping, a flat-pack wardrobe if you are feeling brave. The most popular Lumen, and our first recommendation for daycare runs.',
specs:[['Wheelbase','2,050 mm'],['Deck load','100 kg'],['Max gross weight','220 kg'],['Own weight','24.5 kg'],['Gears','Pinion C1.12 / Nexus 8'],['From','€3.900']] },
frakt: { no:'N°2', name:'Frakt', type:'Front loader', tag:'the load you can see', tpl:'t-frakt', base:4400,
fig:'FIG. 02 — FRAKT · FRONT LOADER · SCALE 1:8', sheet:'SHEET 02 / 03',
desc:'The box sits low, between your arms, where you can watch it — and the steering stays feather-light even fully packed. The trick is in the head angle, and we tune it with you at the fitting. The courier favourite.',
specs:[['Wheelbase','1,850 mm'],['Box volume','105 L'],['Max gross weight','295 kg'],['Own weight','26 kg'],['Gears','Gates belt / 11-speed'],['From','€4.400']] },
vogn: { no:'N°3', name:'Vogn', type:'Cargo trike', tag:'the workshop on wheels', tpl:'t-vogn', base:4900,
fig:'FIG. 03 — VOGN · CARGO TRIKE · SCALE 1:8', sheet:'SHEET 03 / 03',
desc:'Three wheels and a box the size of a washing-machine drum. It stands on its own while you load it, tracks dead straight with eighty kilos aboard, and still fits in a cargo lift. Built for tradespeople who are done with vans.',
specs:[['Wheelbase','1,760 mm'],['Box volume','120 L'],['Max gross weight','340 kg'],['Own weight','29 kg'],['Gears','Enviolo stepless'],['From','€4.900']] }
};
const PAINTS = [
{n:'Signal Amber', c:'#D9541E'}, {n:'Ink Black', c:'#26211A'}, {n:'Moss Green', c:'#5A6B4E'},
{n:'Oxide Red', c:'#8A3B2A'}, {n:'Fog Grey', c:'#B9BCB4'}
];
const WOODS = [
{n:'Birch ply', c:'#E5C993'}, {n:'Smoked oak', c:'#8B6A4C'}, {n:'Aluminium', c:'#BFC3BE'}
];
const EXTRAS = [
{id:'belt', n:'Gates carbon belt drive', short:'BELT DRIVE', p:390},
{id:'canopy', n:'Rain canopy', short:'RAIN CANOPY', p:240},
{id:'battery',n:'Second battery, 500 Wh', short:'2ND BATTERY', p:420},
{id:'kids', n:'Twin child seats + rails', short:'CHILD SEATS', p:190}
];
/* ---------- build & animate a drawing ---------- */
function makeBike(tplId){
const svg = $('#'+tplId).content.firstElementChild.cloneNode(true);
$$('.wheel', svg).forEach(w => {
const cx=+w.dataset.cx, cy=+w.dataset.cy, r=+w.dataset.r, n=+w.dataset.spokes||12;
for(let i=0;i<n;i++){
const a=(Math.PI*2*i)/n + (i%2)*0.12;
const l=document.createElementNS(NS,'line');
l.setAttribute('x1',(cx+Math.cos(a)*r*0.14).toFixed(1));
l.setAttribute('y1',(cy+Math.sin(a)*r*0.14).toFixed(1));
l.setAttribute('x2',(cx+Math.cos(a)*(r-8)).toFixed(1));
l.setAttribute('y2',(cy+Math.sin(a)*(r-8)).toFixed(1));
l.setAttribute('class','ink spoke d');
w.appendChild(l);
}
});
return svg;
}
function drawBike(svg, {stagger=26, base=180} = {}){
if(RM) return;
const ds = $$('.d', svg), fs = $$('.f', svg).filter(el => !el.classList.contains('canopy'));
ds.forEach((el, i) => {
let len = 220;
try { len = el.getTotalLength(); } catch(e){}
const dash = len + 2;
el.style.strokeDasharray = dash + ' ' + dash;
el.style.strokeDashoffset = dash;
el.animate(
[{strokeDashoffset: dash},{strokeDashoffset: 0}],
{duration: Math.min(1150, Math.max(240, len*1.05)), delay: base + i*stagger,
easing: 'cubic-bezier(.45,.05,.35,1)', fill: 'both'});
});
fs.forEach((el, i) => {
el.animate([{opacity:0},{opacity:1}],
{duration: 380, delay: base + ds.length*stagger + 140 + i*55, easing:'ease-out', fill:'both'});
});
}
function toggleCanopy(svg, on){
const c = svg.querySelector('.canopy');
if(!c) return;
c.getAnimations().forEach(a => a.cancel());
if(on){
c.style.display = 'inline';
if(RM) return;
const len = c.getTotalLength();
c.style.strokeDasharray = len + ' ' + len;
c.animate([{strokeDashoffset: len, fillOpacity: 0},{strokeDashoffset: 0, fillOpacity: .09}],
{duration: 750, easing:'cubic-bezier(.45,.05,.35,1)', fill:'both'});
} else {
c.style.display = 'none';
}
}
/* ---------- hero drawing + annotation callouts ---------- */
{
const svg = makeBike('t-frakt');
$('#heroStage').appendChild(svg);
const DATA = [
{t:'FILLET-BRAZED 4130 CROMO', part:'frame', x:60, y:300, pts:[[252,296],[300,296],[452,361]]},
{t:'105 L BIRCH-PLY BOX', part:'box', x:600, y:96, pts:[[594,92],[552,92],[472,270]]},
{t:'HUB MOTOR · 85 NM', part:'rear', x:700, y:574, pts:[[742,560],[742,468],[694,420]]},
{t:'20″ FRONT / 28″ REAR', part:'front', x:36, y:574, pts:[[44,560],[44,470],[148,470]]}
];
DATA.forEach(c => {
const g = document.createElementNS(NS,'g');
g.setAttribute('class','callout f');
g.dataset.target = c.part;
const pl = document.createElementNS(NS,'polyline');
pl.setAttribute('points', c.pts.map(p => p.join(',')).join(' '));
const dot = document.createElementNS(NS,'circle');
const end = c.pts[c.pts.length-1];
dot.setAttribute('cx', end[0]); dot.setAttribute('cy', end[1]); dot.setAttribute('r','3.5');
const tx = document.createElementNS(NS,'text');
tx.setAttribute('x', c.x); tx.setAttribute('y', c.y);
tx.textContent = c.t;
g.append(pl, dot, tx);
svg.appendChild(g);
});
$$('.callout', svg).forEach(c => {
const t = svg.querySelector('[data-part="' + c.dataset.target + '"]');
if(!t) return;
const on = () => { t.classList.add('hot'); c.classList.add('hot'); };
const off = () => { t.classList.remove('hot'); c.classList.remove('hot'); };
c.addEventListener('mouseenter', on); c.addEventListener('mouseleave', off);
t.addEventListener('mouseenter', on); t.addEventListener('mouseleave', off);
});
drawBike(svg, {stagger:26, base:280});
}
/* ---------- machines section ---------- */
const mStage = $('#mStage'), mFig = $('#mFig'), mCorner = $('#mCorner'),
mTitle = $('#mTitle'), mDesc = $('#mDesc'), mSpecs = $('#mSpecs'),
mBody = $('#machineBody');
function renderMachine(id, animate = true){
const m = MODELS[id];
$$('#modelNav .model-tab').forEach(b => {
const on = b.dataset.model === id;
b.classList.toggle('on', on);
b.setAttribute('aria-selected', on);
});
mStage.querySelectorAll('svg').forEach(s => s.remove());
const svg = makeBike(m.tpl);
mStage.appendChild(svg);
if(animate) drawBike(svg, {stagger:14, base:80});
mFig.textContent = m.fig;
mCorner.textContent = m.sheet;
mBody.classList.add('swap');
setTimeout(() => {
mTitle.innerHTML = m.name + ' <span>— ' + m.tag + '.</span>';
mDesc.textContent = m.desc;
mSpecs.innerHTML = m.specs.map(([k,v]) => '<div class="spec"><span>' + k + '</span><b>' + v + '</b></div>').join('');
$('#cfgLink').dataset.config = id;
mBody.classList.remove('swap');
}, animate ? 190 : 0);
}
$$('#modelNav .model-tab').forEach(b =>
b.addEventListener('click', () => renderMachine(b.dataset.model)));
renderMachine('lang', false);
/* ---------- configurator ---------- */
const state = { model:'frakt', paint:0, wood:0, extras:new Set() };
const cStage = $('#cStage'), cPrice = $('#cPrice'), cSum = $('#cSummary'),
paintName = $('#paintName'), paintTag = $('#paintNameTag');
let shown = 0, total = 0, priceRaf = null;
function tweenPrice(to){
total = to;
const from = shown || to, t0 = performance.now();
if(priceRaf) cancelAnimationFrame(priceRaf);
const step = t => {
const k = Math.min(1, (t - t0) / 480), e = 1 - Math.pow(1 - k, 3);
shown = Math.round(from + (to - from) * e);
cPrice.textContent = money(shown);
if(k < 1) priceRaf = requestAnimationFrame(step);
};
priceRaf = requestAnimationFrame(step);
}
function renderConfigurator(rebuild = false){
const m = MODELS[state.model];
if(rebuild || !cStage.querySelector('svg')){
cStage.querySelectorAll('svg').forEach(s => s.remove());
const svg = makeBike(m.tpl);
cStage.appendChild(svg);
drawBike(svg, {stagger:12, base:60});
}
const svg = cStage.querySelector('svg');
svg.style.setProperty('--frame', PAINTS[state.paint].c);
svg.style.setProperty('--box', WOODS[state.wood].c);
svg.classList.toggle('belted', state.extras.has('belt'));
const kids = svg.querySelector('.kidseat');
if(kids) kids.style.display = state.extras.has('kids') ? 'inline' : 'none';
toggleCanopy(svg, state.extras.has('canopy'));
let price = m.base;
state.extras.forEach(id => price += EXTRAS.find(e => e.id === id).p);
tweenPrice(price);
const bits = [m.type.toUpperCase(), PAINTS[state.paint].n.toUpperCase(), WOODS[state.wood].n.toUpperCase()];
EXTRAS.forEach(e => { if(state.extras.has(e.id)) bits.push(e.short); });
cSum.textContent = bits.join(' · ');
paintName.textContent = PAINTS[state.paint].n;
paintTag.textContent = PAINTS[state.paint].n;
$$('#cModels button').forEach(b => b.classList.toggle('on', b.dataset.model === state.model));
$$('#cfgWood button').forEach(b => b.classList.toggle('on', +b.dataset.wood === state.wood));
$$('#cfgPaint .swatch').forEach((s, i) => s.classList.toggle('on', i === state.paint));
}
$$('#cModels button').forEach(b => b.addEventListener('click', () => {
if(state.model !== b.dataset.model){ state.model = b.dataset.model; renderConfigurator(true); }
}));
$$('#cfgPaint .swatch').forEach((s, i) => s.addEventListener('click', () => {
state.paint = i; renderConfigurator();
}));
$$('#cfgWood button').forEach(b => b.addEventListener('click', () => {
state.wood = +b.dataset.wood; renderConfigurator();
}));
$$('#cfgExtras input').forEach(inp => inp.addEventListener('change', () => {
inp.checked ? state.extras.add(inp.dataset.extra) : state.extras.delete(inp.dataset.extra);
renderConfigurator();
}));
renderConfigurator();
$$('[data-config]').forEach(b => b.addEventListener('click', () => {
state.model = b.dataset.config;
renderConfigurator(true);
$('#build').scrollIntoView({behavior: RM ? 'auto' : 'smooth'});
}));
/* ---------- modal + toast ---------- */
const overlay = $('#overlay'), mnav = $('#mnav');
let lastFocus = null;
function openModal(mode){
lastFocus = document.activeElement;
const build = mode === 'build';
$('#mKicker').textContent = build ? 'BUILD REQUEST' : 'WORKSHOP VISIT';
$('#mTitle').textContent = build ? 'Request this build' : 'Book a workshop visit';
const sum = $('#mSummary');
if(build){
sum.hidden = false;
sum.textContent = cSum.textContent + ' · ' + cPrice.textContent;
$('#fMsg').value = composeMessage();
} else {
sum.hidden = true;
$('#fMsg').value = '';
}
$('#mForm').hidden = false;
$('#mSuccess').hidden = true;
$('#mSubmit').disabled = false;
$('#mSubmit').textContent = 'Send request';
$$('.field', overlay).forEach(f => f.classList.remove('bad'));
overlay.hidden = false;
requestAnimationFrame(() => overlay.classList.add('open'));
document.documentElement.style.overflow = 'hidden';
setTimeout(() => $('#fName').focus(), 200);
}
function closeModal(){
overlay.classList.remove('open');
document.documentElement.style.overflow = '';
setTimeout(() => { overlay.hidden = true; }, 280);
if(lastFocus) lastFocus.focus();
}
function composeMessage(){
const m = MODELS[state.model];
const extras = [...state.extras].map(id => EXTRAS.find(e => e.id === id).n.toLowerCase());
return 'Hej Lumen — I\u2019d like to build a ' + m.name + ' (' + m.type.toLowerCase() + ') in ' +
PAINTS[state.paint].n.toLowerCase() + ' with a ' + WOODS[state.wood].n.toLowerCase() + ' box' +
(extras.length ? ', plus ' + extras.join(', ') : '') +
'. Estimated price ' + money(total) + '.';
}
$$('[data-open]').forEach(b => b.addEventListener('click', () => openModal(b.dataset.open)));
$('#reqBtn').addEventListener('click', () => openModal('build'));
$('#visitBook').addEventListener('click', () => openModal('visit'));
$('#mClose').addEventListener('click', closeModal);
$('#sClose').addEventListener('click', closeModal);
overlay.addEventListener('click', e => { if(e.target === overlay) closeModal(); });
document.addEventListener('keydown', e => {
if(e.key === 'Escape'){
if(!overlay.hidden) closeModal();
if(mnav.classList.contains('open')) toggleMenu(false);
}
});
function bad(input, on){
input.closest('.field').classList.toggle('bad', on);
}
$('#mForm').addEventListener('submit', e => {
e.preventDefault();
const name = $('#fName'), email = '#fEmail' && $('#fEmail');
let ok = true;
if(!name.value.trim()){ bad(name, true); ok = false; }
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)){ bad(email, true); ok = false; }
if(!ok) return;
const btn = $('#mSubmit');
btn.disabled = true;
btn.textContent = 'SENDING…';
setTimeout(() => {
$('#mForm').hidden = true;
$('#mSuccess').hidden = false;
$('#sTitle').textContent = 'Tak — request received.';
$('#sText').textContent = 'We\u2019ll reply within two working days from hello@lumencycles.dk. Your configuration travels with the message, so the workshop can start drafting straight away.';
toast('Request sent — talk soon.');
}, 900);
});
const toastEl = $('#toast');
let toastT = null;
function toast(msg){
$('#toastMsg').textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastT);
toastT = setTimeout(() => toastEl.classList.remove('show'), 4200);
}
/* ---------- voices ---------- */
const QUOTES = [
{q:'Two kids, a week of groceries and a ladder. It climbs the bridge without me noticing any of it.', a:'Mette F. · Nørrebro · Lumen Lang, 2021'},
{q:'It paid for itself against van hire in eight months. The box swallows tool chests whole.', a:'Jonas K. · Vesterbro · Lumen Frakt, 2023'},
{q:'They spent two hours moving my saddle four millimetres. That\u2019s the whole review.', a:'Anders L. · Islands Brygge · Lumen Vogn, 2024'},
{q:'Our Frakt has done 9,000 km of daycare runs. The paint has one stone chip. One.', a:'Signe & Mikkel · Østerbro · Lumen Frakt, 2019'}
];
const qBox = $('#quoteBox'), qText = $('#qText'), qAttr = $('#qAttr'), qIdx = $('#qIdx');
let qi = 0, qTimer = null;
function renderQuote(i){
qi = (i + QUOTES.length) % QUOTES.length;
qBox.classList.add('fade');
setTimeout(() => {
qText.textContent = '\u201C' + QUOTES[qi].q + '\u201D';
qAttr.textContent = QUOTES[qi].a;
qIdx.textContent = String(qi+1).padStart(2,'0') + ' / ' + String(QUOTES.length).padStart(2,'0');
qBox.classList.remove('fade');
}, 260);
}
function autoQuotes(){ clearInterval(qTimer); qTimer = setInterval(() => renderQuote(qi+1), 7000); }
$('#qPrev').addEventListener('click', () => { renderQuote(qi-1); autoQuotes(); });
$('#qNext').addEventListener('click', () => { renderQuote(qi+1); autoQuotes(); });
$('#voices').addEventListener('mouseenter', () => clearInterval(qTimer));
$('#voices').addEventListener('mouseleave', autoQuotes);
autoQuotes();
/* ---------- header, menu, reveal, nav state ---------- */
const head = $('#siteHead');
addEventListener('scroll', () => head.classList.toggle('scrolled', scrollY > 24), {passive:true});
function toggleMenu(open){
mnav.classList.toggle('open', open);
$('#menuBtn').setAttribute('aria-expanded', open);
document.documentElement.style.overflow = open ? 'hidden' : '';
}
$('#menuBtn').addEventListener('click', () => toggleMenu(!mnav.classList.contains('open')));
$('#mnavClose').addEventListener('click', () => toggleMenu(false));
$$('.mnav a').forEach(a => a.addEventListener('click', () => toggleMenu(false)));
const io = new IntersectionObserver(es => es.forEach(en => {
if(en.isIntersecting){ en.target.classList.add('in'); io.unobserve(en.target); }
}), {threshold:.12});
$$('.reveal').forEach(el => io.observe(el));
const nio = new IntersectionObserver(es => es.forEach(en => {
const link = $('.site-nav a[href="#' + en.target.id + '"]');
if(link) link.classList.toggle('cur', en.isIntersecting);
}), {rootMargin:'-40% 0px -55% 0px'});
['machines','build','process','voices','visit'].forEach(id => {
const s = document.getElementById(id);
if(s) nio.observe(s);
});
})();
</script>
<!-- ======= modal ======= -->
<div class="overlay" id="overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
<button class="modal-x" id="mClose" type="button" aria-label="Close">✕</button>
<p class="kicker" style="margin-bottom:14px"><svg class="lamp-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg><span id="mKicker">BUILD REQUEST</span></p>
<h3 id="mTitle" style="font-size:1.7rem;margin-bottom:20px">Request this build</h3>
<div class="m-summary mono" id="mSummary"></div>
<form id="mForm" novalidate>
<label class="field"><span class="f-label">Name</span>
<input id="fName" type="text" autocomplete="name">
<span class="err">Please add your name.</span></label>
<label class="field"><span class="f-label">Email</span>
<input id="fEmail" type="email" autocomplete="email">
<span class="err">That email doesn't look right.</span></label>
<label class="field"><span class="f-label">Message</span>
<textarea id="fMsg" rows="4"></textarea></label>
<button class="btn btn-accent" id="mSubmit" type="submit" style="width:100%;justify-content:center">Send request</button>
<p class="form-note mono" style="margin-top:14px">WE REPLY WITHIN TWO WORKING DAYS · NO NEWSLETTERS, EVER</p>
</form>
<div class="m-success" id="mSuccess" hidden>
<svg viewBox="0 0 24 24" width="42" height="42" style="margin:0 auto 18px;color:var(--accent)" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4M5.2 5.2l2.4 2.4M16.4 16.4l2.4 2.4M18.8 5.2l-2.4 2.4M7.6 16.4l-2.4 2.4"/></g></svg>
<h3 id="sTitle" style="font-size:1.5rem;margin-bottom:12px">Tak — request received.</h3>
<p id="sText"></p>
<button class="btn" id="sClose" type="button" style="margin-top:8px">Close</button>
</div>
</div>
</div>
<div class="toast" id="toast" role="status">
<svg viewBox="0 0 24 24" width="14" height="14" style="color:var(--accent);flex:none" aria-hidden="true"><circle cx="12" cy="12" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 2.4v3.4M12 18.2v3.4M2.4 12h3.4M18.2 12h3.4"/></g></svg>
<span id="toastMsg"></span>
</div>
</body>
</html>

