Run details
- Model
- sakana/fugu-max
- Route
- OpenRouter API, one user message
- Served by
- Sakana AI
- Effort
- high, OpenRouter's default for this model
- Limits
- 60 min · 128,000 output tokens (the model's limit)
- Date
- 1 Nov 2026
- Time
- 10 min 38 s
- Size
- 71 kB
- Tokens
- 192 in, 67,514 out (44,589 of them thinking)
- List price
- $0.41 at today's price ($2.00 in / $6.00 out per M)
- Paid
- $0.41
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
71 kB 1,352 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lumen Cycles — Custom cargo bikes, built in København</title>
<meta name="description" content="Lumen Cycles hand-builds long-john cargo bikes to order in Nørrebro, Copenhagen. Measured to your body, your street and your Saturday load.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'%3E%3Ccircle cx='14' cy='14' r='11' fill='none' stroke='%23211E1A' stroke-width='3'/%3E%3Ccircle cx='14' cy='14' r='4' fill='%23BC4B26'/%3E%3C/svg%3E">
<style>
/* ============ Foundation ============ */
:root{
--paper:#F1EDE3;
--paper2:#EAE4D4;
--ink:#211E1A;
--ink2:#5B564B;
--line:#D6CEBB;
--accent:#BC4B26;
--boxfill:#E8DFC7;
--serif:"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
--mono:ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,"Liberation Mono",monospace;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:92px;color-scheme:light}
body{
background:var(--paper);color:var(--ink);
font:400 17px/1.65 var(--serif);
-webkit-font-smoothing:antialiased;
}
::selection{background:var(--accent);color:var(--paper)}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
img,svg{display:block;max-width:100%}
a{color:inherit}
button{font:inherit;color:inherit;background:none;border:none;cursor:pointer}
.wrap{max-width:1180px;margin:0 auto;padding-inline:clamp(20px,5vw,56px)}
.mono{font-family:var(--mono)}
.eyebrow{
font:600 11px/1 var(--mono);letter-spacing:.24em;text-transform:uppercase;
color:var(--accent);display:flex;align-items:center;gap:14px;
}
.eyebrow::before{content:"";width:36px;height:1px;background:var(--accent);flex:none}
h2{font:400 clamp(30px,4.4vw,52px)/1.08 var(--serif);letter-spacing:-.01em;margin:18px 0 14px}
h2 em{font-style:italic;color:var(--accent)}
p{max-width:64ch}
/* subtle print grain */
body::after{
content:"";position:fixed;inset:0;pointer-events:none;z-index:5;opacity:.05;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* reveal on scroll */
[data-reveal]{opacity:0;transform:translateY(26px);transition:opacity .8s cubic-bezier(.2,.6,.2,1),transform .8s cubic-bezier(.2,.6,.2,1);transition-delay:var(--d,0s)}
[data-reveal].revealed{opacity:1;transform:none}
@keyframes softIn{to{opacity:1}}
@keyframes drawIn{to{stroke-dashoffset:0}}
/* ============ Buttons & links ============ */
.btn{
display:inline-flex;align-items:center;gap:12px;
font:600 12px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
padding:17px 26px;border:1px solid var(--ink);
background:var(--ink);color:var(--paper);text-decoration:none;
transition:background .25s,color .25s,border-color .25s;
}
.btn:hover{background:var(--accent);border-color:var(--accent);color:var(--paper)}
.btn.ghost{background:transparent;color:var(--ink)}
.btn.ghost:hover{background:var(--ink);color:var(--paper)}
.link-arrow{
display:inline-flex;align-items:center;gap:10px;
font:600 12px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
color:var(--ink);text-decoration:none;padding:6px 0;
border-bottom:1px solid var(--ink);
transition:color .2s,border-color .2s;
}
.link-arrow svg{transition:transform .25s}
.link-arrow:hover{color:var(--accent);border-color:var(--accent)}
.link-arrow:hover svg{transform:translateX(5px)}
.link-arrow svg{stroke:var(--accent)}
/* ============ Header ============ */
header.site{
position:fixed;inset:0 0 auto;z-index:100;height:72px;
display:flex;align-items:center;
transition:background .3s,box-shadow .3s;
}
header.site.scrolled{background:var(--paper);box-shadow:0 1px 0 var(--line)}
header.site .wrap{width:100%;display:flex;align-items:center;justify-content:space-between;gap:20px}
.brand{display:flex;align-items:center;gap:12px;text-decoration:none}
.brand svg{width:30px;height:30px;color:var(--ink)}
.brand .wm{font:400 22px/1 var(--serif);letter-spacing:.01em}
.brand .wm small{display:block;font:600 8.5px/1 var(--mono);letter-spacing:.34em;color:var(--ink2);margin-top:3px}
nav.main{display:flex;align-items:center;gap:30px}
nav.main a{
font:600 11px/1 var(--mono);letter-spacing:.18em;text-transform:uppercase;
text-decoration:none;color:var(--ink2);padding:6px 0;border-bottom:1px solid transparent;
transition:color .2s,border-color .2s;
}
nav.main a:hover{color:var(--ink);border-color:var(--accent)}
nav.main .btn{padding:13px 20px}
#menuBtn{display:none;width:44px;height:44px;position:relative}
#menuBtn span,#menuBtn span::before{
position:absolute;left:11px;width:22px;height:2px;background:var(--ink);content:"";transition:transform .3s,top .3s;
}
#menuBtn span{top:19px}
#menuBtn span::before{top:8px;left:0}
body.menu-open #menuBtn span{transform:rotate(45deg);top:23px}
body.menu-open #menuBtn span::before{transform:rotate(-45deg);top:0}
#mobileNav{
position:fixed;inset:0;z-index:90;background:var(--paper);
display:flex;flex-direction:column;justify-content:center;padding:0 clamp(24px,8vw,64px);
visibility:hidden;opacity:0;transition:opacity .35s,visibility .35s;
}
body.menu-open #mobileNav{visibility:visible;opacity:1}
#mobileNav a{font:400 clamp(34px,9vw,54px)/1.5 var(--serif);text-decoration:none}
#mobileNav a:hover{color:var(--accent)}
#mobileNav .m-addr{margin-top:42px;font:600 11px/1.9 var(--mono);letter-spacing:.18em;color:var(--ink2);text-transform:uppercase}
/* ============ Hero ============ */
.hero{padding:150px 0 0}
.hero .grid{display:grid;grid-template-columns:1.35fr 1fr;gap:clamp(28px,5vw,80px);align-items:end}
.hero h1{
font:400 clamp(44px,7.4vw,92px)/1.02 var(--serif);letter-spacing:-.02em;
margin-top:26px;
}
.hero h1 em{font-style:italic;color:var(--accent)}
.hero .lede{font-size:19px;color:var(--ink);margin-bottom:30px}
.cta-row{display:flex;gap:14px;flex-wrap:wrap}
.hero-note{margin-top:26px;font:600 10.5px/1.7 var(--mono);letter-spacing:.2em;color:var(--ink2)}
/* drafting sheet */
.sheet{margin-top:64px;border:1.5px solid var(--ink);background:var(--paper2);padding:clamp(14px,2.5vw,30px)}
.sheet figcaption{
display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;
border-top:1px solid var(--line);margin-top:14px;padding-top:12px;
font:600 10px/1.6 var(--mono);letter-spacing:.2em;color:var(--ink2);text-transform:uppercase;
}
.sheet figcaption span:last-child{color:var(--accent)}
/* ============ Bike drawing ============ */
.bike{width:100%;height:auto;color:var(--ink)}
.bike line,.bike path,.bike circle,.bike rect,.bike text{stroke-linecap:round;stroke-linejoin:round}
.bike .tube{stroke:var(--frame,var(--ink));fill:none}
.bike .ink{stroke:var(--ink);fill:none}
.bike .tire{stroke:var(--ink);fill:none;stroke-width:8.5}
.bike .rim{stroke:var(--ink);fill:none;stroke-width:3}
.bike .spokes line{stroke:var(--ink);stroke-width:2}
.bike .hub{fill:var(--paper2);stroke:var(--ink);stroke-width:3}
.bike .refl{fill:var(--accent);stroke:var(--ink);stroke-width:1.4}
.bike .ground{stroke:var(--ink);stroke-width:2}
.bike .road-dash{stroke:var(--accent);stroke-width:3;opacity:.9}
.bike .boxln{stroke:var(--ink);fill:none}
.bike .boxfill{fill:var(--boxfill)}
.bike .boxword{font:600 14px var(--mono);letter-spacing:.42em;fill:var(--ink);text-anchor:middle}
.bike .pivot{fill:var(--paper2);stroke:var(--ink);stroke-width:2.5}
.bike .chain{stroke:var(--ink);fill:none;stroke-dasharray:6 5;stroke-width:2.5}
.bike .crank{stroke:var(--ink);stroke-width:6}
.bike .pedal{stroke:var(--ink);stroke-width:7}
.bike .far{opacity:.32}
.bike .bb{fill:var(--paper2);stroke:var(--ink);stroke-width:3}
.bike .saddle{fill:var(--ink)}
.bike .bell{fill:var(--paper2);stroke:var(--accent);stroke-width:2.5}
.bike .lampfront,.bike .lamprear{fill:var(--accent);stroke:var(--ink);stroke-width:1.4}
.bike .can{stroke:var(--ink);fill:none}
.bike .sheetborder{fill:none;stroke:var(--ink);stroke-width:1.5}
.bike .beam{display:none}
.bike.dyn .beam{display:block}
.bike .beam line{stroke:var(--accent);stroke-width:2.5;opacity:.75}
.bike .annot line,.bike .annot path,.bike .annot rect{stroke:var(--ink);stroke-width:1;fill:none}
.bike .annot text{font:500 11px var(--mono);fill:var(--ink);letter-spacing:.08em;stroke:none}
.bike .tblock rect{fill:var(--paper2)}
.bike-hero{--frame:var(--accent)}
.bike[data-box="closed"] .g-box-upper,.bike[data-box="canopy"] .g-box-upper{display:block}
.bike[data-box="canopy"] .g-canopy{display:block}
.bike .g-box-upper,.bike .g-canopy{display:none}
/* draw-on animation (hero only) */
.bike-hero .draw{stroke-dasharray:1 1;stroke-dashoffset:1;animation:drawIn 1.1s cubic-bezier(.5,0,.2,1) forwards;animation-delay:calc(var(--i,0)*.13s)}
.bike-hero .soft{opacity:0;animation:softIn .6s ease forwards;animation-delay:calc(.7s + var(--i,0)*.1s)}
.bike-hero .annot{opacity:0;animation:softIn .8s ease 1.7s forwards}
/* ============ Ticker ============ */
.ticker{border-block:1px solid var(--line);overflow:hidden;margin-top:0}
.ticker-track{display:flex;width:max-content;animation:tick 34s linear infinite}
.ticker:hover .ticker-track{animation-play-state:paused}
.ticker ul{display:flex;align-items:center;list-style:none;white-space:nowrap;padding:15px 0}
.ticker li{
font:600 11px/1 var(--mono);letter-spacing:.2em;color:var(--ink2);
padding:0 26px;display:flex;align-items:center;gap:52px;text-transform:uppercase;
}
.ticker li::after{content:"";width:5px;height:5px;background:var(--accent);transform:rotate(45deg);flex:none}
@keyframes tick{to{transform:translateX(-50%)}}
/* ============ Sections ============ */
section.block{padding:clamp(80px,10vw,130px) 0}
section.rule{border-top:1px solid var(--line)}
.sec-head{margin-bottom:clamp(36px,5vw,64px);max-width:760px}
.sec-head .lede{font-size:18px;color:var(--ink2)}
/* ---- Models (ledger accordion) ---- */
.model{border-top:1px solid var(--line)}
.model:last-of-type{border-bottom:1px solid var(--line)}
.model-head{
width:100%;display:grid;grid-template-columns:70px 1fr auto 44px;
gap:22px;align-items:center;text-align:left;padding:30px 0;
}
.m-no{font:600 12px var(--mono);letter-spacing:.2em;color:var(--ink2)}
.m-name{font:400 clamp(24px,3.2vw,34px)/1.1 var(--serif);transition:transform .3s,color .3s}
.m-name i{font-style:italic}
.m-tag{display:block;font:600 10.5px/1.7 var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--ink2);margin-top:7px}
.model-head:hover .m-name{color:var(--accent);transform:translateX(8px)}
.m-price{font:600 14px var(--mono);letter-spacing:.08em;justify-self:end}
.m-x{position:relative;width:16px;height:16px;justify-self:end}
.m-x::before,.m-x::after{content:"";position:absolute;background:var(--ink);left:7px;top:0;width:2px;height:16px;transition:transform .35s}
.m-x::after{transform:rotate(90deg)}
.model.open .m-x::before,.model.open .m-x::after{transform:rotate(45deg)}
.model.open .m-x::after{transform:rotate(135deg)}
.model-body{display:grid;grid-template-rows:0fr;transition:grid-template-rows .5s cubic-bezier(.2,.6,.2,1)}
.model.open .model-body{grid-template-rows:1fr}
.model-inner{overflow:hidden}
.model-grid{display:grid;grid-template-columns:1.2fr .9fr;gap:clamp(24px,4vw,64px);padding:4px 0 40px 92px}
.model-grid p{color:var(--ink2);font-size:16.5px}
.model-grid .link-arrow{margin-top:26px}
dl.specs{display:grid;grid-template-columns:auto 1fr;gap:0 24px;font-family:var(--mono)}
dl.specs dt{font:600 10px/1 var(--mono);letter-spacing:.18em;color:var(--ink2);text-transform:uppercase;padding:11px 0;border-bottom:1px solid var(--line)}
dl.specs dd{font:600 12.5px/1.3 var(--mono);text-align:right;padding:10px 0;border-bottom:1px solid var(--line)}
/* ---- Process ---- */
.proc{display:grid;grid-template-columns:.9fr 1.6fr;gap:clamp(32px,6vw,100px)}
.proc-left{position:sticky;top:110px;align-self:start}
.proc-left .mono-note{margin-top:26px;font:600 10.5px/1.8 var(--mono);letter-spacing:.2em;color:var(--ink2);text-transform:uppercase}
ol.steps{list-style:none}
ol.steps li{position:relative;padding:0 0 54px 108px}
ol.steps li:last-child{padding-bottom:0}
ol.steps li::before{content:"";position:absolute;left:23px;top:52px;bottom:6px;width:1px;background:var(--line);transition:background 1s .3s}
ol.steps li:last-child::before{display:none}
.s-no{
position:absolute;left:0;top:0;width:46px;height:46px;
border:1px solid var(--ink);display:grid;place-items:center;
font:600 13px var(--mono);letter-spacing:.1em;
transition:background .5s,color .5s;
}
ol.steps li h3{font:400 clamp(21px,2.4vw,27px)/1.2 var(--serif);padding-top:9px;margin-bottom:10px}
ol.steps li p{color:var(--ink2);font-size:16.5px;max-width:52ch}
ol.steps li.on .s-no{background:var(--ink);color:var(--paper)}
ol.steps li.on::before{background:var(--accent)}
/* ---- Configurator (dark) ---- */
section.dark{background:var(--ink);color:var(--paper)}
section.dark .eyebrow{color:var(--accent)}
section.dark .sec-head .lede{color:#ABA495}
.config{display:grid;grid-template-columns:1.15fr 1fr;gap:clamp(28px,4vw,64px);align-items:start}
.preview{position:sticky;top:100px}
.preview-sheet{border:1px solid rgba(241,237,227,.35);background:var(--paper2);padding:20px 20px 8px}
#specLine{margin-top:18px;font:600 11px/1.9 var(--mono);letter-spacing:.16em;color:var(--paper);text-transform:uppercase}
#leadLine{margin-top:6px;font:600 10.5px/1.8 var(--mono);letter-spacing:.16em;color:#ABA495;text-transform:uppercase}
.optgroup{margin-bottom:34px}
.og-label{font:600 10.5px/1 var(--mono);letter-spacing:.24em;color:#ABA495;text-transform:uppercase;margin-bottom:12px}
.opt{
display:grid;grid-template-columns:auto 1fr auto;gap:16px;align-items:center;
padding:15px 2px;border-bottom:1px solid rgba(241,237,227,.16);cursor:pointer;
}
.opt input{position:absolute;opacity:0;pointer-events:none}
.mark{width:15px;height:15px;border:1px solid var(--paper);position:relative;flex:none}
.opt input:checked ~ .mark{box-shadow:inset 0 0 0 4px var(--accent);border-color:var(--paper)}
.opt input:focus-visible + .mark{outline:2px solid var(--accent);outline-offset:3px}
.opt-name{font:400 18px/1.2 var(--serif)}
.opt-sub{display:block;font:600 10px/1.8 var(--mono);letter-spacing:.16em;color:#ABA495;text-transform:uppercase;margin-top:3px}
.opt-price{font:600 12px var(--mono);letter-spacing:.06em;color:var(--paper)}
.opt.inc .opt-price{color:var(--accent)}
.opt:hover .opt-name{color:var(--accent)}
.swatches{display:flex;gap:14px;flex-wrap:wrap;padding:6px 0 2px}
.sw{width:32px;height:32px;background:var(--c);border:1px solid rgba(241,237,227,.45)}
.sw[aria-pressed="true"]{outline:2px solid var(--paper);outline-offset:3px}
.sw:hover{transform:translateY(-2px)}
#colourNote{margin-top:12px;font:600 10.5px/1.7 var(--mono);letter-spacing:.18em;color:#ABA495;text-transform:uppercase}
.total{border-top:1px solid rgba(241,237,227,.3);padding-top:26px}
.total-row{display:flex;justify-content:space-between;align-items:baseline;gap:20px}
.total-row span{font:600 11px/1 var(--mono);letter-spacing:.22em;color:#ABA495}
.total-row strong{font:600 clamp(30px,3.4vw,38px)/1 var(--mono);letter-spacing:.02em;transition:color .3s}
.total-row strong.flash{color:var(--accent)}
.total-sub{margin-top:12px;font-size:15px;color:#ABA495;max-width:44ch}
#reserveBtn{margin-top:24px;width:100%;justify-content:center}
section.dark .btn{background:var(--paper);color:var(--ink);border-color:var(--paper)}
section.dark .btn:hover{background:var(--accent);border-color:var(--accent);color:var(--paper)}
/* ---- Story / workshop ---- */
.story{display:grid;grid-template-columns:1.5fr 1fr;gap:clamp(32px,6vw,96px)}
.story .prose p{margin-bottom:22px;color:var(--ink);font-size:17px}
.story .prose p:first-of-type::first-letter{
font-size:3.6em;line-height:.82;float:left;padding:6px 12px 0 0;color:var(--accent);font-style:italic;
}
.stamp-wrap{display:flex;flex-direction:column;gap:8px}
.stamp{width:clamp(120px,14vw,158px);margin-bottom:24px}
.stamp-rot{transform-box:fill-box;transform-origin:center;animation:spin 40s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.stamp text{font:600 9px var(--mono);letter-spacing:.16em;fill:var(--ink)}
.facts{list-style:none}
.facts li{border-top:1px solid var(--line);padding:16px 0;display:grid;grid-template-columns:64px 1fr;gap:16px;align-items:baseline}
.facts li:last-child{border-bottom:1px solid var(--line)}
.facts b{font:600 22px/1 var(--mono);color:var(--accent)}
.facts span{font:600 10.5px/1.7 var(--mono);letter-spacing:.18em;color:var(--ink2);text-transform:uppercase}
blockquote.pull{
margin:clamp(60px,8vw,100px) auto 0;max-width:860px;text-align:center;
padding:clamp(40px,5vw,64px) 0;border-block:1px solid var(--line);
}
blockquote.pull p{font:400 clamp(24px,3.4vw,38px)/1.3 var(--serif);margin:0 auto 24px;max-width:26em}
blockquote.pull p::before{content:"“";color:var(--accent)}
blockquote.pull p::after{content:"”";color:var(--accent)}
blockquote.pull cite{font:600 10.5px/1 var(--mono);letter-spacing:.22em;color:var(--ink2);text-transform:uppercase;font-style:normal}
/* ---- Visit ---- */
.visit{display:grid;grid-template-columns:1.3fr 1fr;gap:clamp(32px,6vw,96px);align-items:start}
.open-now{display:flex;align-items:center;gap:12px;font:600 11.5px/1 var(--mono);letter-spacing:.18em;text-transform:uppercase;margin:28px 0 8px}
.open-now .dot{width:9px;height:9px;border-radius:50%;background:var(--accent);flex:none}
.open-now.closed .dot{background:var(--ink2)}
.hours{display:grid;grid-template-columns:auto 1fr;gap:2px 32px;margin-top:18px;font-family:var(--mono)}
.hours dt{font:600 10.5px/2.3 var(--mono);letter-spacing:.2em;color:var(--ink2);text-transform:uppercase}
.hours dd{font:600 12.5px/2.3 var(--mono);text-align:right}
.addr{margin-top:26px;font:600 12px/2 var(--mono);letter-spacing:.12em;text-transform:uppercase}
.addr span{color:var(--ink2)}
.visit .cta-row{margin-top:34px}
.map{border:1px solid var(--line);background:var(--paper2)}
.map svg{width:100%;height:auto}
.map .st{stroke:var(--ink);stroke-width:1.4;opacity:.55;fill:none}
.map .main{stroke:var(--ink);stroke-width:4;fill:none}
.map .gg{stroke:var(--ink);stroke-width:2.4;fill:none}
.map text{font:600 8.5px var(--mono);letter-spacing:.14em;fill:var(--ink2)}
.map .you{fill:var(--accent)}
.map .ping{fill:none;stroke:var(--accent);stroke-width:1.6;transform-box:fill-box;transform-origin:center;animation:ping 2.4s ease-out infinite}
@keyframes ping{0%{transform:scale(.45);opacity:.9}100%{transform:scale(2.6);opacity:0}}
.map .park{stroke:var(--ink2);stroke-width:1;opacity:.7}
/* ---- FAQ ---- */
.faq{max-width:860px}
details{border-top:1px solid var(--line)}
details:last-of-type{border-bottom:1px solid var(--line)}
summary{
list-style:none;display:flex;justify-content:space-between;align-items:center;gap:24px;
padding:26px 0;cursor:pointer;font:400 clamp(19px,2.2vw,23px)/1.3 var(--serif);
}
summary::-webkit-details-marker{display:none}
summary .pm{position:relative;width:15px;height:15px;flex:none}
summary .pm::before,summary .pm::after{content:"";position:absolute;left:6.5px;top:0;width:2px;height:15px;background:var(--accent);transition:transform .3s}
summary .pm::after{transform:rotate(90deg)}
details[open] summary .pm::after{transform:rotate(0)}
details .faq-a{padding:0 0 30px;max-width:60ch;color:var(--ink2);animation:softIn .45s ease both}
details[open] summary{color:var(--accent)}
.faq-foot{margin-top:34px;font:600 10.5px/1.8 var(--mono);letter-spacing:.2em;color:var(--ink2);text-transform:uppercase}
.faq-foot a{color:var(--accent)}
/* ---- Footer ---- */
footer.site{background:var(--ink);color:var(--paper);padding:clamp(70px,9vw,110px) 0 34px}
.foot-mark{display:flex;align-items:baseline;gap:22px;flex-wrap:wrap;border-bottom:1px solid rgba(241,237,227,.2);padding-bottom:36px}
.foot-mark strong{font:400 clamp(64px,12vw,150px)/.9 var(--serif);letter-spacing:-.02em}
.foot-mark span{font:600 11px/1.8 var(--mono);letter-spacing:.3em;color:#ABA495;text-transform:uppercase}
.foot-cols{display:grid;grid-template-columns:1.2fr 1fr 1.4fr;gap:clamp(28px,5vw,72px);padding:52px 0}
.foot-cols h4{font:600 10.5px/1 var(--mono);letter-spacing:.26em;color:#ABA495;text-transform:uppercase;margin-bottom:20px}
.foot-cols ul{list-style:none}
.foot-cols li{margin-bottom:10px;font:600 12px/1.7 var(--mono);letter-spacing:.1em}
.foot-cols a{color:var(--paper);text-decoration:none;border-bottom:1px solid rgba(241,237,227,.3)}
.foot-cols a:hover{color:var(--accent);border-color:var(--accent)}
.foot-cols p{color:#ABA495;font-size:15px;line-height:1.7}
.news-form{display:flex;gap:0;margin-top:20px;border:1px solid rgba(241,237,227,.4)}
.news-form input{
flex:1;background:none;border:none;padding:15px 16px;color:var(--paper);
font:600 12px var(--mono);letter-spacing:.08em;min-width:0;
}
.news-form input::placeholder{color:#7d776a}
.news-form input:focus{outline:none}
.news-form:focus-within{outline:2px solid var(--accent);outline-offset:2px}
.news-form button{background:var(--paper);color:var(--ink);padding:0 22px;font:600 11px var(--mono);letter-spacing:.16em;transition:background .25s,color .25s}
.news-form button:hover{background:var(--accent);color:var(--paper)}
.news-ok{display:flex;gap:12px;align-items:center;margin-top:20px;font:600 12px/1.6 var(--mono);letter-spacing:.1em;color:var(--paper)}
.news-ok svg{flex:none;stroke:var(--accent)}
.foot-legal{
border-top:1px solid rgba(241,237,227,.2);padding-top:26px;
display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap;
font:600 10px/1.9 var(--mono);letter-spacing:.16em;color:#ABA495;text-transform:uppercase;
}
/* ---- Modal ---- */
.modal{position:fixed;inset:0;z-index:300;display:none;align-items:center;justify-content:center;padding:20px}
.modal.open{display:flex}
.modal-back{position:absolute;inset:0;background:rgba(24,22,19,.6)}
.modal-panel{
position:relative;background:var(--paper);border:1.5px solid var(--ink);
width:min(540px,100%);max-height:calc(100vh - 40px);overflow:auto;
padding:clamp(26px,4vw,44px);animation:pop .35s cubic-bezier(.2,.7,.3,1.1) both;
}
@keyframes pop{from{opacity:0;transform:translateY(26px)}}
.modal-x{position:absolute;top:14px;right:14px;width:40px;height:40px;display:grid;place-items:center}
.modal-x:hover{color:var(--accent)}
.modal-panel h3{font:400 clamp(28px,4vw,36px)/1.1 var(--serif);margin:12px 0 10px}
.modal-sub{color:var(--ink2);font-size:15.5px;margin-bottom:26px}
.field{margin-bottom:18px}
.field label{display:block;font:600 10.5px/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--ink2);margin-bottom:8px}
.field input,.field select{
width:100%;background:transparent;border:1px solid var(--ink);color:var(--ink);
padding:13px 14px;font:600 13px var(--mono);letter-spacing:.04em;border-radius:0;
}
.field input:focus,.field select:focus{outline:2px solid var(--accent);outline-offset:1px}
.field .err{display:none;font:600 10.5px/1.5 var(--mono);letter-spacing:.1em;color:var(--accent);margin-top:7px}
.field.bad input{border-color:var(--accent)}
.field.bad .err{display:block}
.modal-panel .btn{width:100%;justify-content:center;margin-top:8px}
.modal-done{text-align:center;padding:14px 0 4px}
.modal-done svg{margin:0 auto 20px;stroke:var(--accent)}
.modal-done h3{margin-bottom:8px}
.modal-done p{color:var(--ink2);font-size:15.5px}
.modal-done .ref{margin-top:18px;font:600 11px/1.8 var(--mono);letter-spacing:.18em;color:var(--ink);text-transform:uppercase}
.modal-done .btn{margin-top:26px}
/* ---- Toast ---- */
#toasts{position:fixed;left:24px;bottom:24px;z-index:400;display:flex;flex-direction:column;gap:10px}
.toast{
background:var(--ink);color:var(--paper);border-left:3px solid var(--accent);
font:600 11.5px/1.5 var(--mono);letter-spacing:.1em;padding:15px 20px;max-width:340px;
opacity:0;transform:translateY(14px);transition:opacity .35s,transform .35s;
}
.toast.in{opacity:1;transform:none}
/* ---- Responsive ---- */
@media (max-width:1020px){
.config{grid-template-columns:1fr}
.preview{position:static}
.proc{grid-template-columns:1fr}
.proc-left{position:static}
}
@media (max-width:880px){
nav.main{display:none}
#menuBtn{display:block}
.hero .grid{grid-template-columns:1fr;align-items:start}
.story,.visit{grid-template-columns:1fr}
.model-head{grid-template-columns:44px 1fr auto 30px;gap:14px}
.model-grid{grid-template-columns:1fr;padding-left:0}
ol.steps li{padding-left:78px}
.s-no{width:38px;height:38px}
ol.steps li::before{left:18px}
.foot-cols{grid-template-columns:1fr 1fr}
}
@media (max-width:560px){
.foot-cols{grid-template-columns:1fr}
.cta-row .btn{width:100%;justify-content:center}
}
@media (prefers-reduced-motion:reduce){
html{scroll-behavior:auto}
*,*::before,*::after{animation-duration:.001s !important;animation-delay:0s !important;transition-duration:.001s !important}
.ticker-track{animation:none}
[data-reveal]{opacity:1;transform:none}
.bike-hero .draw,.bike-hero .soft,.bike-hero .annot{animation:none;opacity:1;stroke-dashoffset:0}
}
</style>
</head>
<body>
<a href="#main" style="position:absolute;left:-9999px">Skip to content</a>
<header class="site" id="siteHeader">
<div class="wrap">
<a class="brand" href="#top" aria-label="Lumen Cycles — home">
<svg viewBox="0 0 28 28" aria-hidden="true">
<circle cx="14" cy="14" r="11" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M14 3v22M3 14h22M6.2 6.2l15.6 15.6M21.8 6.2L6.2 21.8" stroke="currentColor" stroke-width="1.3"/>
<circle cx="14" cy="14" r="2.8" fill="var(--accent)"/>
</svg>
<span class="wm">Lumen<small>CYCLES · KØBENHAVN</small></span>
</a>
<nav class="main" aria-label="Primary">
<a href="#models">Models</a>
<a href="#process">The build</a>
<a href="#configure">Build yours</a>
<a href="#visit">Visit</a>
<a href="#faq">FAQ</a>
<button class="btn" data-book>Book a fitting</button>
</nav>
<button id="menuBtn" aria-label="Menu" aria-expanded="false"><span></span></button>
</div>
</header>
<nav id="mobileNav" aria-label="Mobile">
<a href="#models">Models</a>
<a href="#process">The build</a>
<a href="#configure">Build yours</a>
<a href="#workshop">Workshop</a>
<a href="#visit">Visit</a>
<a href="#faq">FAQ</a>
<p class="m-addr">Guldbergsgade 102 · 2200 København N<br>Tues–Fri 10–18 · Sat 10–15</p>
</nav>
<main id="main">
<!-- ============ HERO ============ -->
<section class="hero" id="top">
<div class="wrap grid">
<div>
<p class="eyebrow" data-reveal>Custom cargo bikes · Nørrebro, København</p>
<h1 data-reveal style="--d:.08s">Built to carry the life you <em>actually</em> live.</h1>
</div>
<div data-reveal style="--d:.16s">
<p class="lede">Every Lumen starts with a tape measure and a conversation, and ends six weeks later as one frame fitted to one household — the school run, the weekly shop, the dog, the occasional flat-pack.</p>
<div class="cta-row">
<a class="btn" href="#configure">Build yours</a>
<button class="btn ghost" data-book>Book a fitting</button>
</div>
<p class="hero-note">From €3,590 · 6–8 weeks · 250 kg haul rating</p>
</div>
</div>
<figure class="sheet wrap" data-reveal style="--d:.2s">
<div id="heroBike" aria-hidden="true"></div>
<figcaption>
<span>Fig. 01 — The L1 Kompas, drawn for client №413</span>
<span>Scroll — the wheels turn</span>
</figcaption>
</figure>
</section>
<!-- ============ TICKER ============ -->
<div class="ticker" aria-hidden="true">
<div class="ticker-track">
<ul>
<li>Hand-built in Nørrebro</li><li>6–8 weeks, measured to delivered</li>
<li>3-year frame warranty</li><li>250 kg total load</li>
<li>Test rides every Saturday</li><li>412 frames since 2016</li>
<li>Colour never costs extra</li>
</ul>
<ul>
<li>Hand-built in Nørrebro</li><li>6–8 weeks, measured to delivered</li>
<li>3-year frame warranty</li><li>250 kg total load</li>
<li>Test rides every Saturday</li><li>412 frames since 2016</li>
<li>Colour never costs extra</li>
</ul>
</div>
</div>
<!-- ============ MODELS ============ -->
<section class="block" id="models">
<div class="wrap">
<div class="sec-head" data-reveal>
<p class="eyebrow">01 — The fleet</p>
<h2>Three frames. <em>No filler.</em></h2>
<p class="lede">Every model starts on the same jig and the same CroMo tube — what changes is the geometry, and the life it's asked to carry.</p>
</div>
<article class="model" data-reveal>
<button class="model-head" aria-expanded="false" aria-controls="m-l1">
<span class="m-no">01</span>
<span class="m-name">L1 <i>Kompas</i><span class="m-tag">The classic long-john · school run to flat-pack</span></span>
<span class="m-price">from €3,890</span>
<span class="m-x" aria-hidden="true"></span>
</button>
<div class="model-body" id="m-l1">
<div class="model-inner"><div class="model-grid">
<div>
<p>Our flagship, and the drawing on the wall. Two kids and the weekly shop, or a dog and a wardrobe — the low box keeps the load between the wheels, so a full Kompas handles like an empty one. Fitted with a 90-litre pine box, and the geometry sheet drawn to your inseam before a single tube is cut.</p>
<button class="link-arrow" data-configure="l1">Configure the Kompas
<svg width="16" height="10" viewBox="0 0 16 10" fill="none" stroke-width="1.6"><path d="M0 5h14M10 1l4 4-4 4"/></svg>
</button>
</div>
<dl class="specs">
<dt>Total load</dt><dd>250 kg</dd>
<dt>Box</dt><dd>90 L pine</dd>
<dt>Wheels</dt><dd>26″ / 20″</dd>
<dt>Drivetrain</dt><dd>Nexus 8 hub</dd>
<dt>Weight</dt><dd>31.4 kg</dd>
<dt>Warranty</dt><dd>3 yr frame</dd>
</dl>
</div></div>
</div>
</article>
<article class="model" data-reveal>
<button class="model-head" aria-expanded="false" aria-controls="m-l2">
<span class="m-no">02</span>
<span class="m-name">L2 <i>Fyr</i><span class="m-tag">Compact long-john · cellars, courtyards, tight lifts</span></span>
<span class="m-price">from €3,590</span>
<span class="m-x" aria-hidden="true"></span>
</button>
<div class="model-body" id="m-l2">
<div class="model-inner"><div class="model-grid">
<div>
<p>Same bones, 26 centimetres shorter. The Fyr exists because we got tired of watching beautiful bikes live in beautiful courtyards that happen to have a 90-degree corner and a Victorian cellar door. It turns inside its own length, carries a crate and a kid, and forgets it's a cargo bike on the way home.</p>
<button class="link-arrow" data-configure="l2">Configure the Fyr
<svg width="16" height="10" viewBox="0 0 16 10" fill="none" stroke-width="1.6"><path d="M0 5h14M10 1l4 4-4 4"/></svg>
</button>
</div>
<dl class="specs">
<dt>Total load</dt><dd>200 kg</dd>
<dt>Box</dt><dd>60 L pine</dd>
<dt>Wheels</dt><dd>24″ / 20″</dd>
<dt>Drivetrain</dt><dd>Nexus 8 hub</dd>
<dt>Weight</dt><dd>27.9 kg</dd>
<dt>Warranty</dt><dd>3 yr frame</dd>
</dl>
</div></div>
</div>
</article>
<article class="model" data-reveal>
<button class="model-head" aria-expanded="false" aria-controls="m-c1">
<span class="m-no">03</span>
<span class="m-name">C1 <i>Havn</i><span class="m-tag">Courier spec · belt drive, dynamo, box lock</span></span>
<span class="m-price">from €4,390</span>
<span class="m-x" aria-hidden="true"></span>
</button>
<div class="model-body" id="m-c1">
<div class="model-inner"><div class="model-grid">
<div>
<p>Built to earn its keep eight hours a day, in Danish Febfast weather, ridden by someone who doesn't baby it. Belt drive and hub gears that don't care about salt, dynamo lighting wired through the frame, a lockable box, and oversized tubing where the courier fleet taught us frames actually break. Mai rode one for nine years. It's still going.</p>
<button class="link-arrow" data-configure="c1">Configure the Havn
<svg width="16" height="10" viewBox="0 0 16 10" fill="none" stroke-width="1.6"><path d="M0 5h14M10 1l4 4-4 4"/></svg>
</button>
</div>
<dl class="specs">
<dt>Total load</dt><dd>240 kg</dd>
<dt>Box</dt><dd>105 L locked</dd>
<dt>Wheels</dt><dd>26″ / 20″</dd>
<dt>Drivetrain</dt><dd>Gates belt + 8</dd>
<dt>Weight</dt><dd>33.2 kg</dd>
<dt>Warranty</dt><dd>3 yr frame</dd>
</dl>
</div></div>
</div>
</article>
</div>
</section>
<!-- ============ PROCESS ============ -->
<section class="block rule" id="process">
<div class="wrap proc">
<div class="proc-left" data-reveal>
<p class="eyebrow">02 — The build</p>
<h2>From tape measure to <em>first ride.</em></h2>
<p class="lede">Six weeks, five visits if you want them, and one frame that fits. Nothing moves to the next bench until the last one is signed off.</p>
<p class="mono-note">Every frame signed by its welder</p>
</div>
<ol class="steps">
<li data-reveal><span class="s-no">01</span>
<h3>Measuring & fitting</h3>
<p>Sixty minutes at the workshop. We measure you on the jig bike, then measure your life: the stairwell, the cellar door, the school gate, the crate you'll actually carry. Bring the kids — there's chalk outside.</p>
</li>
<li data-reveal><span class="s-no">02</span>
<h3>Drawings & deposit</h3>
<p>You leave a €500 deposit and we stay up late with a pencil. A week later your geometry sheet arrives — drawn like the one at the top of this page, with your name in the title block.</p>
</li>
<li data-reveal><span class="s-no">03</span>
<h3>The frame</h3>
<p>CroMo tube, cut for your inseam and the camber of your street, TIG-welded in the jig that's held true since frame №001. Your name is pencilled inside the steerer column before it's tacked.</p>
</li>
<li data-reveal><span class="s-no">04</span>
<h3>Paint, pine & wheels</h3>
<p>Powder coat in any colour we stock — colour never costs extra. Jonas builds the box from Swedish pine; the wheels are laced by hand, three-cross, the way they should be.</p>
</li>
<li data-reveal><span class="s-no">05</span>
<h3>The handover ride</h3>
<p>We ride it to your door, tune it on your kerb, and don't leave until the saddle's right. The first service, at 500 km, is on us.</p>
</li>
</ol>
</div>
</section>
<!-- ============ CONFIGURATOR ============ -->
<section class="block dark" id="configure">
<div class="wrap">
<div class="sec-head" data-reveal>
<p class="eyebrow">03 — Build yours</p>
<h2>Specify it like you mean it.</h2>
<p class="lede">The same sheet our pencil starts from. Every choice updates the drawing, the ledger and the delivery week — nothing is locked until you've ridden one.</p>
</div>
<div class="config">
<div class="preview" data-reveal>
<div class="preview-sheet" id="configSheet"></div>
<p id="specLine">L1 Kompas · Oxide Red · Closed pine box · Nexus 8 hub</p>
<p id="leadLine">Est. delivery — week of —</p>
</div>
<form class="opts" id="configForm" data-reveal style="--d:.1s" aria-label="Bike configurator">
<div class="optgroup">
<p class="og-label">Model</p>
<label class="opt"><input type="radio" name="cfg-model" value="l1" checked><span class="mark"></span>
<span class="opt-main"><span class="opt-name">L1 Kompas</span><span class="opt-sub">The classic long-john · 26″/20″</span></span>
<span class="opt-price">€3,890</span></label>
<label class="opt"><input type="radio" name="cfg-model" value="l2"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">L2 Fyr</span><span class="opt-sub">Compact · 24″/20″</span></span>
<span class="opt-price">€3,590</span></label>
<label class="opt"><input type="radio" name="cfg-model" value="c1"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">C1 Havn</span><span class="opt-sub">Courier spec · belt & dynamo std</span></span>
<span class="opt-price">€4,390</span></label>
</div>
<div class="optgroup">
<p class="og-label">Frame colour — costs nothing extra</p>
<div class="swatches" role="group" aria-label="Frame colour">
<button type="button" class="sw" data-colour="#DBD3BD" data-name="Bone White" style="--c:#DBD3BD" aria-pressed="false" aria-label="Bone White"></button>
<button type="button" class="sw" data-colour="#26231F" data-name="Ink Black" style="--c:#26231F" aria-pressed="false" aria-label="Ink Black"></button>
<button type="button" class="sw" data-colour="#B8431F" data-name="Oxide Red" style="--c:#B8431F" aria-pressed="true" aria-label="Oxide Red"></button>
<button type="button" class="sw" data-colour="#33556B" data-name="Fjord Blue" style="--c:#33556B" aria-pressed="false" aria-label="Fjord Blue"></button>
<button type="button" class="sw" data-colour="#5C6248" data-name="Moss" style="--c:#5C6248" aria-pressed="false" aria-label="Moss"></button>
</div>
<p id="colourNote">Oxide Red · our house colour</p>
</div>
<div class="optgroup">
<p class="og-label">Cargo box</p>
<label class="opt"><input type="radio" name="cfg-box" value="open"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Open bed</span><span class="opt-sub">Aluminium rail · crates & cargo</span></span>
<span class="opt-price">+ €0</span></label>
<label class="opt"><input type="radio" name="cfg-box" value="closed" checked><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Closed pine box</span><span class="opt-sub">90 L · lockable lid</span></span>
<span class="opt-price">+ €180</span></label>
<label class="opt"><input type="radio" name="cfg-box" value="canopy"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Family canopy</span><span class="opt-sub">Pine box + rain hood · two seats</span></span>
<span class="opt-price">+ €340</span></label>
</div>
<div class="optgroup">
<p class="og-label">Drivetrain</p>
<label class="opt"><input type="radio" name="cfg-drive" value="nexus" checked><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Shimano Nexus 8</span><span class="opt-sub">Hub gear · shifts standing still</span></span>
<span class="opt-price">+ €0</span></label>
<label class="opt"><input type="radio" name="cfg-drive" value="deore"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Deore 11-speed</span><span class="opt-sub">Derailleur · for the hillier commute</span></span>
<span class="opt-price">+ €290</span></label>
<label class="opt"><input type="radio" name="cfg-drive" value="belt"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Gates belt + Nexus 8</span><span class="opt-sub">Silent · no oil · winter-proof</span></span>
<span class="opt-price">+ €520</span></label>
</div>
<div class="optgroup">
<p class="og-label">Extras</p>
<label class="opt"><input type="checkbox" id="ex-dyn"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Dynamo light set</span><span class="opt-sub">Front & rear, wired through the frame</span></span>
<span class="opt-price" data-extra-price>+ €190</span></label>
<label class="opt"><input type="checkbox" id="ex-lock"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Frame lock</span><span class="opt-sub">Rear wheel · keys cut at the bench</span></span>
<span class="opt-price" data-extra-price>+ €90</span></label>
<label class="opt"><input type="checkbox" id="ex-cover"><span class="mark"></span>
<span class="opt-main"><span class="opt-name">Rain cover</span><span class="opt-sub">For the box, and for the cargo</span></span>
<span class="opt-price" data-extra-price>+ €70</span></label>
</div>
<div class="total">
<div class="total-row"><span>Total, incl. VAT</span><strong id="totalPrice">€4,070</strong></div>
<p class="total-sub">€500 deposit reserves your slot · balance on delivery · fitted and delivered by hand, in person.</p>
<button type="button" class="btn" id="reserveBtn">Reserve this build — €500</button>
</div>
</form>
</div>
</div>
</section>
<!-- ============ WORKSHOP / STORY ============ -->
<section class="block" id="workshop">
<div class="wrap">
<div class="story">
<div data-reveal>
<p class="eyebrow">04 — The workshop</p>
<h2 style="margin-bottom:30px">Two people, one jig, <em>no shortcuts.</em></h2>
<div class="prose">
<p>Lumen opened in 2016, in a former bakery on Guldbergsgade, with one welding jig, two people and a stubborn idea: that a cargo bike should fit its rider the way a good boot fits a walker.</p>
<p>Mai spent nine years as a Copenhagen courier before she learned to build; Jonas was a cabinetmaker who kept volunteering to make better boxes. Four hundred frames later the division of labour still holds — she welds, he builds the pine, and both of us answer the door.</p>
<p>Nothing here is chosen from a catalogue page you can't see. Tubes are cut for your inseam and the slope of your street; the box is sized to the crate you actually own. And if we can't build what you need well, we'll tell you whose bike to buy instead.</p>
</div>
</div>
<div class="stamp-wrap" data-reveal style="--d:.12s">
<svg class="stamp" viewBox="0 0 120 120" aria-hidden="true">
<defs><path id="stc" d="M60 60 m-45 0 a45 45 0 1 1 90 0 a45 45 0 1 1 -90 0"/></defs>
<g class="stamp-rot"><text><textPath href="#stc">LUMEN CYCLES · KØBENHAVN · EST 2016 ·</textPath></text></g>
<circle cx="60" cy="60" r="30" fill="none" stroke="var(--ink)" stroke-width="1.5"/>
<path d="M60 32v56M32 60h56M40.2 40.2l39.6 39.6M79.8 40.2L40.2 79.8" stroke="var(--ink)" stroke-width="1.3"/>
<circle cx="60" cy="60" r="7" fill="none" stroke="var(--ink)" stroke-width="1.5"/>
<circle cx="60" cy="60" r="2.6" fill="var(--accent)"/>
</svg>
<ul class="facts">
<li><b>412</b><span>frames built since 2016</span></li>
<li><b>0</b><span>shipped without a final test ride</span></li>
<li><b>1</b><span>jig, unchanged since frame №001</span></li>
</ul>
</div>
</div>
<blockquote class="pull" data-reveal>
<p>We sold the second car eight months after the Kompas arrived. Nobody has missed it once.</p>
<cite>Karina L. — Frederiksberg · two kids, one dog</cite>
</blockquote>
</div>
</section>
<!-- ============ VISIT ============ -->
<section class="block rule" id="visit">
<div class="wrap visit">
<div data-reveal>
<p class="eyebrow">05 — Visit</p>
<h2>Come see the jig.</h2>
<p class="lede">The demo fleet is outside, the coffee is inside, and there's chalk for the kids. Fittings take an hour and cost nothing — book below, or just knock on a Tuesday.</p>
<p class="open-now" id="openNow"><span class="dot"></span><span id="openLabel">—</span></p>
<dl class="hours">
<dt>Tuesday — Friday</dt><dd>10.00 – 18.00</dd>
<dt>Saturday</dt><dd>10.00 – 15.00 · test rides</dd>
<dt>Sunday — Monday</dt><dd>Closed · we're welding</dd>
</dl>
<p class="addr">Værkstedet — Guldbergsgade 102<br>2200 København N<br><span>Five minutes from Nørrebrogade</span></p>
<div class="cta-row">
<button class="btn" data-book>Book a fitting</button>
<a class="btn ghost" href="#faq">Read the FAQ</a>
</div>
</div>
<div class="map" data-reveal style="--d:.12s" aria-label="Map of the workshop location">
<svg viewBox="0 0 400 300">
<path class="st" d="M0 40H400M0 120H400M0 250H400M70 0V300M180 0V300M330 0V300"/>
<path class="main" d="M0 292L400 48"/>
<text x="30" y="150" transform="rotate(-33 30 150)">NØRREBROGADE</text>
<path class="gg" d="M0 176H400"/>
<text x="8" y="166">GULDBERGSGADE</text>
<g class="park">
<rect x="300" y="196" width="82" height="58" fill="none"/>
<path d="M300 254L382 196M300 224L330 196M352 254L382 226"/>
<text x="306" y="212">ASSISTENS</text>
</g>
<circle class="ping" cx="248" cy="176" r="7"/>
<circle class="you" cx="248" cy="176" r="6"/>
<rect x="240" y="186" width="58" height="15" fill="var(--ink)"/>
<text x="244" y="197" fill="var(--paper)" stroke="none">LUMEN</text>
</svg>
</div>
</div>
</section>
<!-- ============ FAQ ============ -->
<section class="block rule" id="faq">
<div class="wrap">
<div class="sec-head" data-reveal>
<p class="eyebrow">06 — Questions</p>
<h2>Asked at the door, <em>answered here.</em></h2>
</div>
<div class="faq" data-reveal>
<details>
<summary>Will it fit in my life — cellar, courtyard, stairwell?<span class="pm" aria-hidden="true"></span></summary>
<div class="faq-a"><p>Almost certainly, and we check before we cut. At the fitting we measure where the bike will live and sleep. If the stairwell is tight, the L2 Fyr cuts 26 cm off the wheelbase; if there's truly nowhere, we'll say so and suggest a lock-up plan instead. We've never shipped a bike that couldn't get home.</p></div>
</details>
<details>
<summary>Can it genuinely replace a car?<span class="pm" aria-hidden="true"></span></summary>
<div class="faq-a"><p>For a Copenhagen household, usually yes. A Kompas is rated to 250 kg all-in, which covers two kids, the weekly shop and a bag of footballs. Our honest limits: moving a sofa, the trip to Jutland with grandma's dresser, and weeks of sideways sleet — good rain gear and studded tyres solve the last one better than a roof does.</p></div>
</details>
<details>
<summary>What about Danish winters?<span class="pm" aria-hidden="true"></span></summary>
<div class="faq-a"><p>Hub gears and the belt-drive option don't care about salt, and nothing on a Lumen seizes after a February. We deliver with a studded-tyre option at cost, and your first winter service — full strip, regrease, spoke check — is on us.</p></div>
</details>
<details>
<summary>Servicing and warranty?<span class="pm" aria-hidden="true"></span></summary>
<div class="faq-a"><p>Three-year warranty on the frame, one year on parts, and a free service at 500 km. After that, everything on the bike is standard, industry-available equipment — any competent shop can service it, though the coffee is better here.</p></div>
</details>
<details>
<summary>Can I spread the cost?<span class="pm" aria-hidden="true"></span></summary>
<div class="faq-a"><p>A €500 deposit reserves your build slot, with the balance on delivery. If you'd rather pay over 10 to 36 months, our finance partner arranges it at the fitting — usually a two-minute form, and the bike is still yours, not leased.</p></div>
</details>
<details>
<summary>What if my cargo changes?<span class="pm" aria-hidden="true"></span></summary>
<div class="faq-a"><p>Boxes bolt on and off the same four mounts. Trade your open bed for a canopy box when kid number two arrives, or back again — we refit secondhand boxes at cost, and old Lumen boxes have a waiting list of their own.</p></div>
</details>
<p class="faq-foot">Something else? <a href="#visit">Ask us at the workshop</a> — or on a Saturday, from the saddle of a demo bike.</p>
</div>
</div>
</section>
</main>
<!-- ============ FOOTER ============ -->
<footer class="site">
<div class="wrap">
<div class="foot-mark">
<strong>Lumen</strong>
<span>Cycles · København<br>Est. 2016</span>
</div>
<div class="foot-cols">
<div>
<h4>Værkstedet</h4>
<ul>
<li>Guldbergsgade 102</li>
<li>2200 København N</li>
<li>Tue–Fri 10–18 · Sat 10–15</li>
<li><a href="#visit">Open-now status</a></li>
</ul>
</div>
<div>
<h4>Explore</h4>
<ul>
<li><a href="#models">The fleet</a></li>
<li><a href="#process">The build</a></li>
<li><a href="#configure">Build yours</a></li>
<li><a href="#faq">Questions</a></li>
</ul>
</div>
<div>
<h4>Ride notes</h4>
<p>One email a month: builds in progress, routes we love, Saturday openings. No noise, ever.</p>
<div id="newsWrap">
<form class="news-form" id="newsForm" novalidate>
<input type="email" id="newsEmail" placeholder="your@email.dk" aria-label="Email address" autocomplete="email">
<button type="submit">Sign up</button>
</form>
</div>
</div>
</div>
<div class="foot-legal">
<span>© <span id="yy">2025</span> Lumen Cycles ApS · CVR 41 22 87 05</span>
<span>All prices include Danish VAT</span>
<span>Designed & built five minutes from Nørrebrogade</span>
</div>
</div>
</footer>
<!-- ============ BOOKING MODAL ============ -->
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
<div class="modal-back" data-close></div>
<div class="modal-panel">
<button class="modal-x" data-close aria-label="Close">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M1 1l16 16M17 1L1 17"/></svg>
</button>
<div id="modalMain">
<p class="eyebrow">Fitting · 60 min · free</p>
<h3 id="mTitle">Book a fitting</h3>
<p class="modal-sub" id="modalSummary">Sixty minutes at Guldbergsgade 102. We'll measure, ride the demo fleet and sketch your build — no obligation, decent coffee.</p>
<form id="bookForm" novalidate>
<div class="field" id="fld-name">
<label for="fName">Your name</label>
<input id="fName" type="text" autocomplete="name">
<p class="err">A name helps us greet you properly.</p>
</div>
<div class="field" id="fld-email">
<label for="fEmail">Email</label>
<input id="fEmail" type="email" autocomplete="email">
<p class="err">That email doesn't look right — try again?</p>
</div>
<div class="field">
<label for="fLoad">What will it carry?</label>
<input id="fLoad" type="text" placeholder="Two kids, groceries, a cello…">
</div>
<div class="field">
<label for="fSlot">When suits?</label>
<select id="fSlot">
<option>Weekday morning</option>
<option>Weekday afternoon</option>
<option>Saturday — test-ride day</option>
</select>
</div>
<button type="submit" class="btn">Request a fitting</button>
</form>
</div>
<div class="modal-done" id="modalDone" hidden>
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" stroke-width="1.8"><circle cx="22" cy="22" r="20"/><path d="M13 22.5l6.5 6.5L31 16"/></svg>
<h3 id="doneTitle">Tak.</h3>
<p id="doneText">Your fitting request is in. We'll confirm by email within one working day.</p>
<p class="ref" id="doneRef"></p>
<button class="btn ghost" data-close style="border-color:var(--ink);color:var(--ink)">Close</button>
</div>
</div>
</div>
<div id="toasts" aria-live="polite"></div>
<script>
(function(){
'use strict';
const $ = s => document.querySelector(s);
const $$ = s => [...document.querySelectorAll(s)];
const EUR = n => '€' + n.toLocaleString('en-US');
/* ============================================================
1. The bike — a parametric technical drawing, injected twice
============================================================ */
function bikeSVG(hero){
const P = v => (+v).toFixed(1);
const dd = n => hero ? ` pathLength="1" style="--i:${n}"` : '';
const D = n => hero ? ` draw` : '';
const sf = n => hero ? ` style="--i:${n}"` : '';
const wheel = (cx,cy,r,si) => {
const rim = r-(r>70?15:13), hub = r>70?11:8, n = r>70?14:12;
let sp='';
for(let k=0;k<n;k++){
const t=.4+k*Math.PI*2/n;
sp+=`<line x1="${P(cx+Math.cos(t)*hub)}" y1="${P(cy+Math.sin(t)*hub)}" x2="${P(cx+Math.cos(t)*(rim-2))}" y2="${P(cy+Math.sin(t)*(rim-2))}"/>`;
}
const ra=(rim-4)*.62, rt=.9;
return `<g class="wheel soft${sf(si)}" data-cx="${cx}" data-cy="${cy}" data-r="${r-4}">
<circle class="tire" cx="${cx}" cy="${cy}" r="${r}"/>
<circle class="rim" cx="${cx}" cy="${cy}" r="${rim}"/>
<g class="spokes">${sp}</g>
<circle class="refl" cx="${P(cx+Math.cos(rt)*ra)}" cy="${P(cy+Math.sin(rt)*ra)}" r="4"/>
<circle class="hub" cx="${cx}" cy="${cy}" r="${hub+1}"/>
</g>`;
};
let s = `<svg class="bike${hero?' bike-hero':''}" data-box="closed" viewBox="0 0 920 470" role="img" aria-label="Side-view technical drawing of a Lumen long-john cargo bike">`;
if(hero) s += `<rect class="sheetborder" x="10" y="10" width="900" height="452"/>`;
/* ground + bike-lane dashes */
s += `<line class="ground soft${sf(2)}" x1="18" y1="398" x2="902" y2="398"/>
<line class="road-dash soft${sf(3)}" x1="18" y1="418" x2="902" y2="418" stroke-dasharray="34 26"/>`;
/* wheels (behind everything) */
s += wheel(152,334,62,0) + wheel(672,304,92,1);
/* frame — painted tubes */
s += `<g>`
+ `<line class="tube${D(0)}"${dd(0)} x1="172" y1="252" x2="322" y2="244" stroke-width="9"/>`
+ `<line class="tube${D(1)}"${dd(1)} x1="318" y1="160" x2="312" y2="232" stroke-width="10"/>`
+ `<line class="tube${D(2)}"${dd(2)} x1="314" y1="228" x2="486" y2="304" stroke-width="9"/>`
+ `<line class="tube${D(3)}"${dd(3)} x1="486" y1="306" x2="540" y2="158" stroke-width="9"/>`
+ `<line class="ink" x1="540" y1="158" x2="550" y2="132" stroke-width="5"/>`
+ `<line class="tube${D(4)}"${dd(4)} x1="536" y1="170" x2="672" y2="304" stroke-width="6"/>`
+ `<line class="tube${D(5)}"${dd(5)} x1="488" y1="306" x2="670" y2="305" stroke-width="7"/>`
+ `<path class="tube${D(6)}"${dd(6)} d="M176 252 C164 272 155 300 152 330" stroke-width="6.5"/>`
+ `<path class="tube${D(7)}"${dd(7)} d="M82.5 315.4 A72 72 0 0 1 221.6 315.4" stroke-width="6"/>`
+ `<path class="tube${D(8)}"${dd(8)} d="M571.5 277.1 A104 104 0 0 1 772.5 277.1" stroke-width="7"/>`
+ `</g>`;
/* handlebar + bell */
s += `<g>`
+ `<line class="ink${D(9)}"${dd(9)} x1="317" y1="163" x2="330" y2="140" stroke-width="6"/>`
+ `<path class="ink${D(10)}"${dd(10)} d="M330 140 C342 129 358 127 372 133" stroke-width="6"/>`
+ `<line class="ink${D(11)}"${dd(11)} x1="371" y1="132" x2="391" y2="137" stroke-width="8"/>`
+ `<circle class="bell soft${sf(7)}" cx="349" cy="121" r="5"/>`
+ `</g>`;
/* steering linkage — the long-john's signature */
s += `<g class="soft${sf(4)}">`
+ `<line class="ink" x1="313" y1="228" x2="178" y2="249" stroke-width="3.5"/>`
+ `<circle class="pivot" cx="313" cy="228" r="4"/><circle class="pivot" cx="178" cy="249" r="4"/>`
+ `</g>`;
/* cargo box — lower walls, upper walls, canopy */
s += `<g class="g-box-lower">`
+ `<rect class="boxfill" x="47" y="211" width="234" height="42"/>`
+ `<line class="boxln${D(12)}"${dd(12)} x1="46" y1="254" x2="46" y2="210" stroke-width="5"/>`
+ `<line class="boxln${D(13)}"${dd(13)} x1="282" y1="254" x2="282" y2="210" stroke-width="5"/>`
+ `<line class="boxln${D(14)}"${dd(14)} x1="40" y1="210" x2="288" y2="210" stroke-width="5"/>`
+ `<line class="boxln soft${sf(9)}" x1="46" y1="254" x2="282" y2="254" stroke-width="3"/>`
+ `<text class="boxword soft${sf(10)}" x="166" y="238">LUMEN</text>`
+ `</g>`
+ `<g class="g-box-upper">`
+ `<rect class="boxfill" x="47" y="169" width="234" height="41"/>`
+ `<line class="boxln${D(15)}"${dd(15)} x1="46" y1="210" x2="46" y2="168" stroke-width="5"/>`
+ `<line class="boxln${D(16)}"${dd(16)} x1="282" y1="210" x2="282" y2="168" stroke-width="5"/>`
+ `<line class="boxln${D(17)}"${dd(17)} x1="40" y1="168" x2="288" y2="168" stroke-width="5"/>`
+ `<line class="boxln soft${sf(9)}" x1="46" y1="190" x2="282" y2="190" stroke-width="2"/>`
+ `</g>`
+ `<g class="g-canopy">`
+ `<path class="can${D(18)}"${dd(18)} d="M52 168 C62 108 190 96 276 168" stroke-width="4"/>`
+ `<path class="can soft${sf(9)}" d="M62 168 C72 120 190 108 266 168" stroke-width="2"/>`
+ `</g>`;
/* saddle */
s += `<path class="saddle soft${sf(8)}" d="M518 130 C528 118 562 116 578 126 C570 134 530 138 518 130 Z"/>`;
/* drivetrain */
s += `<g class="soft${sf(5)}">`
+ `<circle class="ink" cx="486" cy="306" r="27" stroke-width="4"/>`
+ `<circle class="ink" cx="672" cy="304" r="11" stroke-width="3"/>`
+ `<line class="chain" x1="486" y1="279" x2="672" y2="293"/>`
+ `<line class="chain" x1="486" y1="333" x2="672" y2="315"/>`
+ `<line class="crank far" x1="486" y1="306" x2="461" y2="273"/>`
+ `<line class="pedal far" x1="445" y1="273" x2="477" y2="273"/>`
+ `<line class="crank" x1="486" y1="306" x2="511" y2="339"/>`
+ `<line class="pedal" x1="495" y1="339" x2="527" y2="339"/>`
+ `<circle class="bb" cx="486" cy="306" r="7"/>`
+ `</g>`;
/* lamps + dynamo beam (beam shows only with the dynamo extra) */
s += `<g class="soft${sf(6)}">`
+ `<rect class="lampfront" x="30" y="216" width="14" height="10" rx="2"/>`
+ `<rect class="lamprear" x="771" y="256" width="9" height="16" rx="2"/>`
+ `</g>`
+ `<g class="beam"><line x1="28" y1="212" x2="2" y2="203"/><line x1="28" y1="221" x2="2" y2="221"/><line x1="28" y1="230" x2="2" y2="239"/></g>`;
/* drafting-sheet annotations (hero only) */
if(hero){
s += `<g class="annot">`
+ `<line x1="152" y1="437" x2="152" y2="451"/><line x1="672" y1="437" x2="672" y2="451"/>`
+ `<line x1="152" y1="444" x2="340" y2="444"/><line x1="484" y1="444" x2="672" y2="444"/>`
+ `<text x="412" y="448" text-anchor="middle">WHEELBASE 1 240 MM</text>`
+ `<line x1="848" y1="124" x2="864" y2="124"/><line x1="848" y1="398" x2="864" y2="398"/>`
+ `<line x1="856" y1="131" x2="856" y2="205"/><line x1="856" y1="315" x2="856" y2="391"/>`
+ `<text x="849" y="261" text-anchor="middle" transform="rotate(-90 849 261)">SADDLE 780–980 MM</text>`
+ `<path d="M200 168 L250 100 H296"/><text x="302" y="104">CLOSED PINE BOX · 90 L</text>`
+ `<path d="M400 266 L432 96 H470"/><text x="476" y="100">CrMo STEEL · TIG-WELDED</text>`
+ `<path d="M20 26H32M26 20V32"/><path d="M888 26H900M894 20V32"/>`
+ `<path d="M20 456H32M26 450V462"/><path d="M888 456H900M894 450V462"/>`
+ `<g class="tblock"><rect x="662" y="424" width="248" height="36"/>`
+ `<text x="670" y="435">LUMEN CYCLES — SHEET 01 / 01</text>`
+ `<text x="670" y="446">MODEL L1 KOMPAS · SCALE 1:8</text>`
+ `<text x="670" y="457">DRAWN AT GULDBERGSGADE 102 · REV C</text></g>`
+ `</g>`;
}
return s + `</svg>`;
}
$('#heroBike').innerHTML = bikeSVG(true);
$('#configSheet').innerHTML = bikeSVG(false);
/* ============================================================
2. Scroll FX — the bike rides while you scroll
============================================================ */
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const wheels = [];
$$('.bike').forEach(svg => svg.querySelectorAll('.wheel').forEach(w =>
wheels.push({el:w, cx:+w.dataset.cx, cy:+w.dataset.cy, r:+w.dataset.r})));
const dashes = $$('.road-dash');
const header = $('#siteHeader');
let target = 0, cur = 0;
addEventListener('scroll', () => { target = scrollY; }, {passive:true});
function frame(){
if(!reduced){
cur += (target - cur) * .11;
if(Math.abs(target - cur) > .05){
for(const w of wheels){
const deg = cur * .55 * (88 / w.r); /* equal surface speed */
w.el.setAttribute('transform', `rotate(${deg.toFixed(2)} ${w.cx} ${w.cy})`);
}
for(const d of dashes) d.style.strokeDashoffset = (cur * .9).toFixed(1) + 'px';
}
}
header.classList.toggle('scrolled', scrollY > 8);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
/* ============================================================
3. Reveals & the process rail
============================================================ */
const io = new IntersectionObserver(es => es.forEach(e => {
if(e.isIntersecting){ e.target.classList.add('revealed','on'); io.unobserve(e.target); }
}), {threshold:.15});
$$('[data-reveal]').forEach(el => io.observe(el));
/* ============================================================
4. Model ledger (accordion, one open at a time)
============================================================ */
const models = $$('.model');
models.forEach(m => {
m.querySelector('.model-head').addEventListener('click', () => {
const open = m.classList.contains('open');
models.forEach(x => { x.classList.remove('open'); x.querySelector('.model-head').setAttribute('aria-expanded','false'); });
if(!open){ m.classList.add('open'); m.querySelector('.model-head').setAttribute('aria-expanded','true'); }
});
});
/* ============================================================
5. Configurator — live drawing, ledger and delivery week
============================================================ */
const PR = {
model:{l1:3890, l2:3590, c1:4390},
box:{open:0, closed:180, canopy:340},
drive:{nexus:0, deore:290, belt:520},
extras:{dyn:190, lock:90, cover:70}
};
const NAME = {l1:'L1 Kompas', l2:'L2 Fyr', c1:'C1 Havn'};
const BOXL = {open:'Open bed', closed:'Closed pine box', canopy:'Family canopy'};
const DRVL = {nexus:'Nexus 8 hub', deore:'Deore 11-speed', belt:'Gates belt + 8'};
const state = {model:'l1', colour:'#B8431F', colourName:'Oxide Red', box:'closed', drive:'nexus',
extras:{dyn:false, lock:false, cover:false}};
const cfgSvg = $('#configSheet svg');
const totalPrice = $('#totalPrice');
let lastModel = 'l1';
function leadDate(){
const d = new Date(Date.now() + 49 * 864e5); /* seven weeks out */
d.setDate(d.getDate() - ((d.getDay() + 6) % 7)); /* …that Monday */
return d.toLocaleDateString('en-GB', {day:'numeric', month:'long'});
}
function render(){
const isC1 = state.model === 'c1';
let total = PR.model[state.model] + PR.box[state.box] + PR.drive[state.drive];
for(const key of ['dyn','lock','cover']){
const input = $('#ex-' + key);
const row = input.closest('.opt');
const forced = isC1 && (key === 'dyn' || key === 'lock');
if(forced) state.extras[key] = true;
input.checked = state.extras[key];
input.disabled = forced;
row.classList.toggle('inc', forced);
row.querySelector('[data-extra-price]').textContent = forced ? 'Included' : '+ ' + EUR(PR.extras[key]);
if(state.extras[key] && !forced) total += PR.extras[key];
}
if(state.extras.cover) total += 0; /* counted above via loop */
/* drawing */
cfgSvg.dataset.box = state.box;
cfgSvg.style.setProperty('--frame', state.colour);
cfgSvg.classList.toggle('dyn', state.extras.dyn);
/* ledger */
totalPrice.textContent = EUR(total);
totalPrice.classList.remove('flash'); void totalPrice.offsetWidth; totalPrice.classList.add('flash');
const extras = ['dyn','lock','cover'].filter(k => state.extras[k]).length;
$('#specLine').textContent =
`${NAME[state.model]} · ${state.colourName} · ${BOXL[state.box]} · ${DRVL[state.drive]}${extras ? ' · ' + extras + ' extra' + (extras>1?'s':'') : ''}`;
$('#leadLine').textContent = `Est. delivery — week of ${leadDate()}`;
cfgSummary = `${NAME[state.model]} — ${state.colourName} — ${BOXL[state.box]} — ${DRVL[state.drive]} — ${EUR(total)}`;
}
let cfgSummary = '';
$('#configForm').addEventListener('change', e => {
const t = e.target;
if(t.name === 'cfg-model'){
if(lastModel === 'c1') { state.extras.dyn = false; state.extras.lock = false; }
state.model = lastModel = t.value;
}
if(t.name === 'cfg-box') state.box = t.value;
if(t.name === 'cfg-drive') state.drive = t.value;
if(t.id.startsWith('ex-')) state.extras[t.id.slice(3)] = t.checked;
render();
});
$$('.sw').forEach(b => b.addEventListener('click', () => {
$$('.sw').forEach(x => x.setAttribute('aria-pressed','false'));
b.setAttribute('aria-pressed','true');
state.colour = b.dataset.colour; state.colourName = b.dataset.name;
$('#colourNote').textContent = state.colourName + ' · no extra cost';
render();
}));
render();
/* "Configure the …" links from the model ledger */
$$('[data-configure]').forEach(b => b.addEventListener('click', () => {
const v = b.dataset.configure;
const radio = document.querySelector(`input[name="cfg-model"][value="${v}"]`);
if(radio.checked){ /* same model — still jump */ }
else { radio.checked = true; radio.dispatchEvent(new Event('change', {bubbles:true})); }
toast(`${NAME[v]} loaded in the configurator`);
$('#configure').scrollIntoView({behavior: reduced ? 'auto' : 'smooth'});
}));
$('#reserveBtn').addEventListener('click', () => openModal(cfgSummary));
/* ============================================================
6. Booking modal
============================================================ */
const modal = $('#modal');
function openModal(summary){
$('#modalSummary').textContent = summary
? `Reserving: ${summary}. A fitting locks the spec — and the price — before any deposit leaves your account.`
: 'Sixty minutes at Guldbergsgade 102. We\u2019ll measure, ride the demo fleet and sketch your build — no obligation, decent coffee.';
modal.dataset.summary = summary || '';
modal.classList.add('open');
document.body.style.overflow = 'hidden';
setTimeout(() => $('#fName').focus(), 60);
}
function closeModal(){
modal.classList.remove('open');
document.body.style.overflow = '';
setTimeout(() => { /* reset for next visit */
$('#modalDone').hidden = true; $('#modalMain').hidden = false;
$('#bookForm').reset();
$$('.field').forEach(f => f.classList.remove('bad'));
}, 300);
}
$$('[data-book]').forEach(b => b.addEventListener('click', () => openModal('')));
$$('[data-close]').forEach(b => b.addEventListener('click', closeModal));
addEventListener('keydown', e => { if(e.key === 'Escape' && modal.classList.contains('open')) closeModal(); });
$('#bookForm').addEventListener('submit', e => {
e.preventDefault();
const name = $('#fName').value.trim();
const email = $('#fEmail').value.trim();
const okName = name.length >= 2;
const okMail = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email);
$('#fld-name').classList.toggle('bad', !okName);
$('#fld-email').classList.toggle('bad', !okMail);
if(!okName || !okMail) return;
const first = name.split(/\s+/)[0];
const ref = 'LC-' + (1000 + Math.floor(Math.random() * 9000));
$('#doneTitle').textContent = `Tak, ${first}.`;
$('#doneText').textContent =
`Your fitting request for a ${$('#fSlot').value.toLowerCase()} is in — ${modal.dataset.summary ? 'build spec noted, nothing charged yet' : 'no obligation'}. We'll confirm by email within one working day.`;
$('#doneRef').textContent = `Reference ${ref} · Lumen Cycles, Guldbergsgade 102`;
$('#modalMain').hidden = true;
$('#modalDone').hidden = false;
});
/* ============================================================
7. Newsletter
============================================================ */
$('#newsForm').addEventListener('submit', e => {
e.preventDefault();
const email = $('#newsEmail').value.trim();
if(!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)){
$('#newsEmail').focus();
toast('That email doesn\u2019t look right — try again?');
return;
}
$('#newsWrap').innerHTML =
`<p class="news-ok">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke-width="1.6"><circle cx="9" cy="9" r="8"/><path d="M5.5 9.5l2.5 2.5L13 7"/></svg>
Tak — you're on the list. First note lands next month.</p>`;
});
/* ============================================================
8. Live open/closed status (Copenhagen time)
============================================================ */
(function openNow(){
const HOURS = {Mon:null, Tue:[10,18], Wed:[10,18], Thu:[10,18], Fri:[10,18], Sat:[10,15], Sun:null};
const FULL = {Mon:'Monday', Tue:'Tuesday', Wed:'Wednesday', Thu:'Thursday', Fri:'Friday', Sat:'Saturday', Sun:'Sunday'};
let wd, h, m;
try{
const p = new Intl.DateTimeFormat('en-GB',{timeZone:'Europe/Copenhagen',weekday:'short',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).formatToParts(new Date());
wd = p.find(x=>x.type==='weekday').value;
h = +p.find(x=>x.type==='hour').value % 24;
m = +p.find(x=>x.type==='minute').value;
}catch(err){ wd = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][new Date().getDay()]; h = new Date().getHours(); m = new Date().getMinutes(); }
const today = HOURS[wd], t = h + m/60;
const el = $('#openNow'), label = $('#openLabel');
if(today && t >= today[0] && t < today[1]){
label.textContent = `Open now — closes ${today[1]}.00, Copenhagen time`;
}else if(today && t < today[0]){
label.textContent = `Closed — opens today at ${today[0]}.00`;
}else{
const order = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
let i = order.indexOf(wd), found = null;
for(let k=1;k<=7;k++){ const d = order[(i+k)%7]; if(HOURS[d]){ found = d; break; } }
label.textContent = found ? `Closed — opens ${FULL[found]} at ${HOURS[found][0]}.00` : 'Closed';
el.classList.add('closed');
}
})();
/* ============================================================
9. Small utilities — toasts, mobile menu, year
============================================================ */
function toast(msg){
const t = document.createElement('div');
t.className = 'toast'; t.textContent = msg;
$('#toasts').append(t);
requestAnimationFrame(() => t.classList.add('in'));
setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 400); }, 2800);
}
const menuBtn = $('#menuBtn');
menuBtn.addEventListener('click', () => {
const open = document.body.classList.toggle('menu-open');
menuBtn.setAttribute('aria-expanded', open);
});
$$('#mobileNav a').forEach(a => a.addEventListener('click', () => {
document.body.classList.remove('menu-open');
menuBtn.setAttribute('aria-expanded','false');
}));
$('#yy').textContent = new Date().getFullYear();
})();
</script>
</body>
</html>

