feat(website): add Nuxt 4 marketing landing page

New standalone apps/website (Nuxt 4) serving the public marketing site at
dezky.local / www.dezky.local. The customer portal moves off the root domain
to app.dezky.local only.

Landing page ported from the Dezky design handoff: light theme, Danish
default, hero variant A, with a working da/en toggle. Self-contained colour
system threaded through components (utils/landingTokens.ts), full bilingual
copy (utils/landingCopy.ts), and shared state (composables/useLanding.ts).
Sections live under components/landing/* with the Node logo under
components/brand/*.

Wired into docker-compose (website service, volume, Traefik labels, network
aliases) and bootstrap.sh (hosts list + service URLs).
This commit is contained in:
Ronni Baslund
2026-06-05 10:58:25 +02:00
parent 47eb9502f8
commit c9911cc262
35 changed files with 8760 additions and 1 deletions
@@ -0,0 +1,29 @@
<script setup lang="ts">
// Node mark + "dezky" wordmark (JetBrains Mono, tracked tight). Ported from
// logos.jsx NodeLockup + NodeWordmark.
import { C } from '~/utils/landingTokens'
const props = withDefaults(defineProps<{
scale?: number
fg?: string
accent?: string
}>(), {
scale: 1,
fg: C.carbon,
accent: C.signal,
})
</script>
<template>
<div :style="{ display: 'flex', alignItems: 'center', gap: `${18 * scale}px` }">
<BrandNodeMark :size="72 * scale" :fg="fg" :accent="accent" />
<span :style="{
fontFamily: '\'JetBrains Mono\', \'IBM Plex Mono\', ui-monospace, monospace',
fontWeight: 600,
fontSize: `${56 * scale * 0.78}px`,
letterSpacing: '-0.04em',
color: fg,
lineHeight: 0.9,
}">dezky</span>
</div>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
// Dezky "Node" mark — a lowercase d (donut style) inside a squircle, with a
// corner node-dot. Geometry is the locked set from the brand handoff
// (logos.jsx NodeMark + LOCKED). The squircle paints in `fg`; the letterform
// and dot paint in `accent` (electric chartreuse) — the design's intent.
import { C, LOCKED } from '~/utils/landingTokens'
const props = withDefaults(defineProps<{
size?: number
fg?: string
accent?: string
}>(), {
size: 96,
fg: C.carbon,
accent: C.signal,
})
const { bowlR, stemW, dotR, contR } = LOCKED
const overlap = stemW * 0.55
const cy = 52
const cx = 50 - stemW / 2 + overlap / 2
const stemX = cx + bowlR - overlap
const stemRight = stemX + stemW
const capR = stemW / 2
const stemTop = 26
const stemBottom = cy + bowlR
const holeR = Math.max(2.5, bowlR - stemW - 0.5)
const bowlPath =
`M ${cx - bowlR} ${cy} ` +
`a ${bowlR} ${bowlR} 0 1 0 ${bowlR * 2} 0 ` +
`a ${bowlR} ${bowlR} 0 1 0 ${-bowlR * 2} 0 Z`
const counterPath =
`M ${cx - holeR} ${cy} ` +
`a ${holeR} ${holeR} 0 1 0 ${holeR * 2} 0 ` +
`a ${holeR} ${holeR} 0 1 0 ${-holeR * 2} 0 Z`
const stemPath =
`M ${stemX} ${stemTop + capR} ` +
`a ${capR} ${capR} 0 0 1 ${stemW} 0 ` +
`L ${stemRight} ${stemBottom} ` +
`L ${stemX} ${stemBottom} Z`
</script>
<template>
<svg :width="size" :height="size" viewBox="0 0 100 100" aria-label="dezky node mark">
<rect x="8" y="8" width="84" height="84" :rx="contR" :fill="fg" />
<g :fill="accent">
<path :d="`${bowlPath} ${counterPath}`" fill-rule="evenodd" />
<path :d="stemPath" />
</g>
<circle cx="74" cy="26" :r="dotR" :fill="accent" />
</svg>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup lang="ts">
// Landing button. Variants: primary (signal fill), secondary (outline), ghost.
// Ported from landing-sections.jsx Button. Named Btn.vue to avoid colliding
// with any shared <Button> in @dezky/ui.
import { computed } from 'vue'
import { C } from '~/utils/landingTokens'
import { useTheme } from '~/composables/useLanding'
const props = withDefaults(defineProps<{
variant?: 'primary' | 'secondary' | 'ghost'
size?: 'md' | 'lg'
full?: boolean
}>(), {
variant: 'primary',
size: 'md',
full: false,
})
const t = useTheme()
const style = computed(() => {
const base = {
primary: { background: C.signal, color: C.carbon, border: `1px solid ${C.signal}` },
secondary: { background: 'transparent', color: t.value.fg, border: `1px solid ${t.value.borderStrong}` },
ghost: { background: 'transparent', color: t.value.fg, border: '1px solid transparent' },
}[props.variant]
return {
...base,
padding: props.size === 'lg' ? '18px 28px' : '14px 22px',
fontSize: props.size === 'lg' ? '15px' : '14px',
fontWeight: 600,
fontFamily: '\'Inter\', sans-serif',
borderRadius: '4px',
letterSpacing: '-0.005em',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
width: props.full ? '100%' : 'auto',
whiteSpace: 'nowrap',
transition: 'transform 0.1s ease, box-shadow 0.15s ease',
}
})
</script>
<template>
<button :style="style"><slot /></button>
</template>
@@ -0,0 +1,46 @@
<script setup lang="ts">
// Section 05 — comparison table (Dezky vs US hyperscaler).
// Ported from landing-sections.jsx Compare.
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
</script>
<template>
<section :style="{ background: t.bg, color: t.fg }">
<LandingContainer pad="140px 64px">
<LandingSectionLabel :label="copy.compare.label" />
<div :style="{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', gap: '80px', alignItems: 'end', marginBottom: '56px' }">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.compare.heading }}</h2>
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '17px', lineHeight: 1.6, maxWidth: '460px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.compare.lede }}</p>
</div>
<div :style="{ border: `1px solid ${t.borderStrong}` }">
<div :style="{ display: 'grid', gridTemplateColumns: '1.2fr 1fr 1fr', background: t.fg, color: t.bg }">
<div :style="{ padding: '20px 28px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', letterSpacing: '0.12em', textTransform: 'uppercase', opacity: 0.6 }">kategori</div>
<div :style="{ padding: '20px 28px', display: 'flex', alignItems: 'center', gap: '10px', borderLeft: `1px solid ${t.fgDim}` }">
<BrandNodeMark :size="18" :fg="t.bg" :accent="t.signal" />
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '14px', fontWeight: 600 }">{{ copy.compare.cols[0] }}</span>
</div>
<div :style="{ padding: '20px 28px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '14px', fontWeight: 500, opacity: 0.7, borderLeft: `1px solid ${t.fgDim}` }">{{ copy.compare.cols[1] }}</div>
</div>
<div
v-for="(row, i) in copy.compare.rows" :key="i"
:style="{
display: 'grid', gridTemplateColumns: '1.2fr 1fr 1fr',
borderTop: `1px solid ${t.border}`,
background: i % 2 === 0 ? t.bg : t.bgAlt,
fontFamily: '\'Inter\', sans-serif', fontSize: '15px',
}"
>
<div :style="{ padding: '22px 28px', color: t.fgMuted }">{{ row[0] }}</div>
<div :style="{ padding: '22px 28px', color: t.fg, fontWeight: 600, borderLeft: `1px solid ${t.border}`, display: 'flex', alignItems: 'center', gap: '10px' }">
<span :style="{ width: '4px', height: '4px', background: t.signal, borderRadius: '999px', flexShrink: 0 }" />
{{ row[1] }}
</div>
<div :style="{ padding: '22px 28px', color: t.fgMuted, borderLeft: `1px solid ${t.border}` }">{{ row[2] }}</div>
</div>
</div>
</LandingContainer>
</section>
</template>
@@ -0,0 +1,18 @@
<script setup lang="ts">
// Padded, max-width-centred section wrapper. Ported from landing-sections.jsx Container.
withDefaults(defineProps<{
pad?: string
max?: number
}>(), {
pad: '120px 64px',
max: 1280,
})
</script>
<template>
<div :style="{ padding: pad }">
<div :style="{ maxWidth: `${max}px`, margin: '0 auto' }">
<slot />
</div>
</div>
</template>
+34
View File
@@ -0,0 +1,34 @@
<script setup lang="ts">
// Section 09 — FAQ. Native <details> accordion with an animated plus marker
// (.faq-plus styles live in assets/styles/base.css). Ported from
// landing-sections.jsx FAQ.
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
</script>
<template>
<section :style="{ background: t.bg, color: t.fg }">
<LandingContainer pad="140px 64px" :max="960">
<LandingSectionLabel :label="copy.faq.label" />
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.faq.heading }}</h2>
<div :style="{ marginTop: '56px' }">
<details
v-for="(item, i) in copy.faq.items" :key="i"
:style="{
borderTop: `1px solid ${t.borderStrong}`,
borderBottom: i === copy.faq.items.length - 1 ? `1px solid ${t.borderStrong}` : 'none',
}"
>
<summary :style="{ padding: '24px 0', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '24px', listStyle: 'none' }">
<span :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 500, fontSize: '19px', color: t.fg, letterSpacing: '-0.015em' }">{{ item[0] }}</span>
<span class="faq-plus" :style="{ width: '14px', height: '14px', position: 'relative', color: t.fgMuted, flexShrink: 0 }" />
</summary>
<div :style="{ paddingBottom: '28px', paddingRight: '60px' }">
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '15px', lineHeight: 1.6, maxWidth: '720px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ item[1] }}</p>
</div>
</details>
</div>
</LandingContainer>
</section>
</template>
@@ -0,0 +1,32 @@
<script setup lang="ts">
// Final CTA — carbon panel with a faint Node-mark watermark.
// Ported from landing-sections.jsx FinalCTA.
import { C } from '~/utils/landingTokens'
import { useCopy } from '~/composables/useLanding'
const copy = useCopy()
</script>
<template>
<section id="final-cta" :style="{ background: C.carbon, color: C.bone, position: 'relative', overflow: 'hidden', scrollMarginTop: '72px' }">
<div :style="{ position: 'absolute', right: '-180px', bottom: '-180px', opacity: 0.05 }">
<BrandNodeMark :size="640" :fg="C.carbon" :accent="C.signal" />
</div>
<LandingContainer pad="140px 64px">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(48px, 6vw, 96px)', letterSpacing: '-0.04em', lineHeight: 0.98, margin: 0, color: C.bone, textWrap: 'balance', maxWidth: '900px' }">
<template v-for="(part, i) in copy.finalCta.heading" :key="i">
<template v-if="typeof part === 'string'">{{ part }} </template>
<span v-else :style="{ color: C.signal }">{{ part.hl }}</span>
</template>
</h2>
<div :style="{ marginTop: '28px', maxWidth: '520px', fontFamily: '\'Inter\', sans-serif', fontSize: '19px', color: 'rgba(244,243,238,0.7)' }">{{ copy.finalCta.sub }}</div>
<div :style="{ marginTop: '40px' }">
<button :style="{
background: C.signal, color: C.carbon, border: 'none',
padding: '20px 32px', fontFamily: '\'Inter\', sans-serif',
fontSize: '16px', fontWeight: 600, borderRadius: '4px', cursor: 'pointer',
letterSpacing: '-0.005em',
}">{{ copy.finalCta.cta }} </button>
</div>
</LandingContainer>
</section>
</template>
@@ -0,0 +1,60 @@
<script setup lang="ts">
// Footer — lockup + tagline + legal, four link columns, status row.
// Ported from landing-sections.jsx Footer (light mode). Anchor links smooth-
// scroll; "#" placeholders point at not-yet-built subpages.
import { C } from '~/utils/landingTokens'
import { useTheme, useCopy, scrollToAnchor } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
function onLink(e: MouseEvent, href: string) {
if (href.startsWith('#') && href.length > 1) {
e.preventDefault()
scrollToAnchor(href)
} else if (href === '#') {
e.preventDefault()
}
}
</script>
<template>
<footer :style="{ background: C.bone, color: C.carbon, borderTop: `1px solid ${t.border}` }">
<LandingContainer pad="80px 64px 40px">
<div :style="{ display: 'grid', gridTemplateColumns: '1.4fr repeat(4, 1fr)', gap: '48px' }">
<div>
<BrandNodeLockup :scale="0.75" :fg="C.carbon" :accent="C.signal" />
<div :style="{ marginTop: '20px', fontFamily: '\'Inter\', sans-serif', fontSize: '14px', color: 'rgba(10,10,10,0.6)', maxWidth: '280px', lineHeight: 1.5 }">{{ copy.footer.tagline }}</div>
<div :style="{ marginTop: '28px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: 'rgba(10,10,10,0.45)', lineHeight: 1.7 }">
<div>{{ copy.footer.legal.name }}</div>
<div>{{ copy.footer.legal.cvr }}</div>
<div>{{ copy.footer.legal.addr }}</div>
</div>
</div>
<div v-for="(col, i) in copy.footer.cols" :key="i">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: 'rgba(10,10,10,0.45)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '18px' }">{{ col[0] }}</div>
<div :style="{ display: 'flex', flexDirection: 'column', gap: '12px' }">
<a
v-for="(link, j) in col[1]" :key="j" :href="link[1]"
:style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '14px', color: 'rgba(10,10,10,0.78)' }"
@click="onLink($event, link[1])"
>{{ link[0] }}</a>
</div>
</div>
</div>
<div :style="{
marginTop: '64px', paddingTop: '28px', borderTop: `1px solid ${t.border}`,
display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '24px',
fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: 'rgba(10,10,10,0.45)',
}">
<div>{{ copy.footer.copyright }}</div>
<div :style="{ display: 'flex', alignItems: 'center', gap: '24px' }">
<span :style="{ display: 'flex', alignItems: 'center', gap: '8px' }">
<span :style="{ width: '6px', height: '6px', background: C.ok, borderRadius: '999px', boxShadow: `0 0 0 3px ${C.ok}33` }" />
{{ copy.footer.status }}
</span>
<span>v1.0.4</span>
</div>
</div>
</LandingContainer>
</footer>
</template>
+68
View File
@@ -0,0 +1,68 @@
<script setup lang="ts">
// Hero — eyebrow, big headline with signal-yellow highlighter brush, sub,
// dual CTAs, trust strip, and the product viewport mockup below.
// Ported from landing-sections.jsx Hero (variant A, light mode).
import { computed } from 'vue'
import { C } from '~/utils/landingTokens'
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
// Variant A is the production default the user landed on.
const headline = computed(() => copy.value.hero.headlineA)
// Light-mode highlight: a yellow marker swipe sitting low behind the phrase
// (signal-yellow text on bone fails contrast, so we brush instead).
const brush = {
backgroundImage: `linear-gradient(180deg, transparent 0%, transparent 56%, ${C.signal} 56%, ${C.signal} 96%, transparent 96%)`,
padding: '0 0.06em',
boxDecorationBreak: 'clone',
WebkitBoxDecorationBreak: 'clone',
} as const
</script>
<template>
<section :style="{ background: t.bg, color: t.fg, paddingTop: '80px' }">
<LandingContainer pad="60px 64px 0">
<div :style="{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '56px' }">
<span :style="{ width: '6px', height: '6px', borderRadius: '999px', background: t.signal, boxShadow: `0 0 0 4px ${t.signal}33` }" />
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', color: t.fgMuted, letterSpacing: '0.04em', whiteSpace: 'nowrap' }">{{ copy.hero.eyebrow }}</span>
</div>
<h1 :style="{
fontFamily: '\'Inter Tight\', \'Inter\', sans-serif',
fontWeight: 600, fontSize: 'clamp(56px, 7.2vw, 112px)', letterSpacing: '-0.04em',
lineHeight: 0.96, margin: 0, textWrap: 'balance', color: t.fg,
}">
<template v-for="(part, i) in headline" :key="i">
<template v-if="typeof part === 'string'">{{ part }} </template>
<span v-else :style="brush">{{ part.hl }} </span>
</template>
</h1>
<div :style="{ marginTop: '40px', maxWidth: '620px' }">
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '20px', lineHeight: 1.5, maxWidth: '620px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.hero.sub }}</p>
</div>
<div :style="{ display: 'flex', gap: '12px', marginTop: '40px', flexWrap: 'wrap' }">
<LandingBtn variant="primary" size="lg">{{ copy.hero.cta }} </LandingBtn>
<LandingBtn variant="secondary" size="lg">{{ copy.hero.sub_cta }}</LandingBtn>
</div>
<div :style="{
marginTop: '64px', paddingTop: '28px', borderTop: `1px solid ${t.border}`,
display: 'flex', gap: '56px', flexWrap: 'wrap',
}">
<div v-for="(s, i) in copy.hero.trust" :key="i" :style="{ display: 'flex', alignItems: 'center', gap: '10px' }">
<span :style="{ width: '4px', height: '4px', background: t.signal, borderRadius: '999px' }" />
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', color: t.fgMuted, letterSpacing: '0.04em' }">{{ s }}</span>
</div>
</div>
</LandingContainer>
<LandingContainer pad="80px 64px 120px">
<LandingProductMockup />
</LandingContainer>
</section>
</template>
@@ -0,0 +1,31 @@
<script setup lang="ts">
// Section 03 — setup. Three numbered steps on a top rule with node markers.
// Ported from landing-sections.jsx HowItWorks.
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
</script>
<template>
<section :style="{ background: t.bg, color: t.fg }">
<LandingContainer pad="120px 64px">
<LandingSectionLabel :label="copy.how.label" />
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.how.heading }}</h2>
<div :style="{ marginTop: '80px', display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0', position: 'relative' }">
<div
v-for="(step, i) in copy.how.steps" :key="i"
:style="{ padding: '40px 36px 0 0', borderTop: `1px solid ${t.borderStrong}`, position: 'relative' }"
>
<div :style="{
position: 'absolute', top: '-7px', left: '0', width: '14px', height: '14px',
background: i === 0 ? t.signal : t.bg,
border: `1px solid ${t.borderStrong}`, borderRadius: '999px',
}" />
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', color: t.fgDim, letterSpacing: '0.08em' }">step {{ step.n }}</div>
<h3 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '28px', letterSpacing: '-0.025em', lineHeight: 1.1, margin: '20px 0 16px 0', color: t.fg }">{{ step.title }}</h3>
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '15px', lineHeight: 1.6, maxWidth: '300px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ step.body }}</p>
</div>
</div>
</LandingContainer>
</section>
</template>
@@ -0,0 +1,50 @@
<script setup lang="ts">
// Tiny abstract per-module glyph (soft geometric, not literal icons).
// Ported from landing-sections.jsx ModuleGlyph.
import { computed } from 'vue'
import { useTheme } from '~/composables/useLanding'
const props = defineProps<{ name: string }>()
const t = useTheme()
const stroke = 1.5
const kind = computed(() => {
const n = props.name
if (n === 'Mail' || n === 'mail') return 'mail'
if (n === 'Drev' || n === 'Drive') return 'drive'
if (n === 'Møder' || n === 'Meet') return 'meet'
if (n === 'Chat') return 'chat'
return 'identity'
})
</script>
<template>
<svg width="36" height="36" viewBox="0 0 36 36">
<template v-if="kind === 'mail'">
<rect x="3" y="9" width="30" height="20" rx="2" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<path d="M3 11 L18 22 L33 11" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<circle cx="30" cy="9" r="3" :fill="t.signal" />
</template>
<template v-else-if="kind === 'drive'">
<rect x="4" y="10" width="28" height="18" rx="2" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<path d="M4 14 L13 14 L16 10 L32 10" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<circle cx="18" cy="20" r="2.5" :fill="t.signal" />
</template>
<template v-else-if="kind === 'meet'">
<rect x="3" y="10" width="22" height="16" rx="2" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<path d="M25 14 L33 10 L33 26 L25 22 Z" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<circle cx="14" cy="18" r="3" :fill="t.signal" />
</template>
<template v-else-if="kind === 'chat'">
<path d="M5 6 L31 6 Q33 6 33 8 L33 22 Q33 24 31 24 L16 24 L9 30 L9 24 L7 24 Q5 24 5 22 Z" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<circle cx="14" cy="15" r="1.6" :fill="t.signal" />
<circle cx="19" cy="15" r="1.6" :fill="t.fg" />
<circle cx="24" cy="15" r="1.6" :fill="t.fg" />
</template>
<template v-else>
<rect x="8" y="14" width="20" height="16" rx="2" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<path d="M12 14 V10 Q12 5 18 5 Q24 5 24 10 V14" fill="none" :stroke="t.fg" :stroke-width="stroke" />
<circle cx="18" cy="22" r="2.5" :fill="t.signal" />
</template>
</svg>
</template>
+64
View File
@@ -0,0 +1,64 @@
<script setup lang="ts">
// Sticky top nav with logo, anchor links, language toggle, login + demo CTA.
// Ported from landing-sections.jsx Nav (light mode, production subset).
import { computed } from 'vue'
import { APP_URL } from '~/utils/landingTokens'
import { useTheme, useCopy, useLang, toggleLang, scrollToAnchor } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
const lang = useLang()
const items = computed(() => [
{ label: copy.value.nav.product, href: '#suite' },
{ label: copy.value.nav.security, href: '#sovereignty' },
{ label: copy.value.nav.whitelabel, href: '#whitelabel' },
{ label: copy.value.nav.pricing, href: '#pricing' },
{ label: copy.value.nav.docs, href: '#' },
])
function onLogo() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
</script>
<template>
<header :style="{
position: 'sticky', top: '0', zIndex: 100,
background: 'rgba(250,250,247,0.84)',
backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)',
borderBottom: `1px solid ${t.border}`,
}">
<div :style="{
maxWidth: '1280px', margin: '0 auto', padding: '18px 64px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}">
<a href="#" :style="{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer' }" @click.prevent="onLogo">
<BrandNodeMark :size="32" :fg="t.fg" :accent="t.signal" />
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontWeight: 600, fontSize: '16px', letterSpacing: '-0.02em', color: t.fg }">dezky</div>
</a>
<nav :style="{ display: 'flex', alignItems: 'center', gap: '36px' }">
<a
v-for="(it, i) in items" :key="i" :href="it.href"
:style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '14px', color: t.fgMuted, letterSpacing: '-0.005em' }"
@click.prevent="scrollToAnchor(it.href)"
>{{ it.label }}</a>
</nav>
<div :style="{ display: 'flex', alignItems: 'center', gap: '14px' }">
<button
:style="{
background: 'transparent', border: `1px solid ${t.border}`,
color: t.fgMuted, fontSize: '11px', padding: '6px 10px', borderRadius: '4px',
fontFamily: '\'JetBrains Mono\', monospace', letterSpacing: '0.06em', textTransform: 'uppercase',
whiteSpace: 'nowrap',
}"
@click="toggleLang()"
>{{ lang === 'da' ? 'da · en' : 'en · da' }}</button>
<a :href="APP_URL" :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '14px', color: t.fgMuted }">{{ copy.nav.login }}</a>
<LandingBtn variant="primary" @click="scrollToAnchor('#final-cta')">{{ copy.nav.cta }} </LandingBtn>
</div>
</div>
</header>
</template>
@@ -0,0 +1,37 @@
<script setup lang="ts">
// A single "partner brand" row in the whitelabel demo.
// Ported from landing-sections.jsx PartnerCard.
import { computed } from 'vue'
const props = withDefaults(defineProps<{
fg: string
bg: string
border: string
accent: string
name: string
subtitle: string
placeholder?: boolean
}>(), { placeholder: false })
const initial = computed(() => props.name[0].toUpperCase())
</script>
<template>
<div :style="{
background: bg, border: `1px solid ${border}`, borderRadius: '4px',
padding: '20px 22px', display: 'flex', alignItems: 'center', gap: '16px',
opacity: placeholder ? 0.55 : 1,
borderStyle: placeholder ? 'dashed' : 'solid',
}">
<div :style="{
width: '44px', height: '44px', borderRadius: '4px', background: accent,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 700,
fontSize: '22px', color: '#FFF', letterSpacing: '-0.02em', flexShrink: 0,
}">{{ initial }}</div>
<div :style="{ flex: 1 }">
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '17px', color: fg, letterSpacing: '-0.02em' }">{{ name }}</div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: 'rgba(0,0,0,0.5)', marginTop: '2px' }">{{ subtitle }}</div>
</div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', color: 'rgba(0,0,0,0.4)', letterSpacing: '0.1em', textTransform: 'uppercase' }">powered by dezky</div>
</div>
</template>
@@ -0,0 +1,35 @@
<script setup lang="ts">
// Section 08 — pricing. Pitch column + a single price card.
// Ported from landing-sections.jsx Pricing.
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
</script>
<template>
<section id="pricing" :style="{ background: t.bgAlt, color: t.fg, borderTop: `1px solid ${t.border}`, borderBottom: `1px solid ${t.border}`, scrollMarginTop: '72px' }">
<LandingContainer pad="120px 64px">
<LandingSectionLabel :label="copy.pricing.label" />
<div :style="{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '80px', alignItems: 'center' }">
<div>
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.pricing.heading }}</h2>
<div :style="{ marginTop: '28px', maxWidth: '520px' }">
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '20px', lineHeight: 1.5, maxWidth: '640px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.pricing.lede }}</p>
</div>
<div :style="{ marginTop: '36px', display: 'flex', alignItems: 'center', gap: '16px' }">
<LandingBtn variant="primary" size="lg">{{ copy.pricing.cta }} </LandingBtn>
</div>
</div>
<div :style="{ background: t.surface, border: `1px solid ${t.border}`, borderRadius: '6px', padding: '36px 36px' }">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.1em', textTransform: 'uppercase' }">{{ copy.pricing.teaser }}</div>
<div :style="{ display: 'flex', alignItems: 'baseline', gap: '12px', marginTop: '12px' }">
<span :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontSize: '96px', fontWeight: 600, letterSpacing: '-0.045em', lineHeight: 1, color: t.fg }">{{ copy.pricing.price }}</span>
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '13px', color: t.fgMuted }">{{ copy.pricing.unit }}</span>
</div>
<div :style="{ height: '1px', background: t.border, margin: '28px 0' }" />
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '13px', lineHeight: 1.6, maxWidth: '640px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.pricing.note }}</p>
</div>
</div>
</LandingContainer>
</section>
</template>
@@ -0,0 +1,21 @@
<script setup lang="ts">
// Section 01 — the problem. Ported from landing-sections.jsx Problem.
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
</script>
<template>
<section :style="{ background: t.bg, color: t.fg }">
<LandingContainer pad="120px 64px">
<LandingSectionLabel :label="copy.problem.label" />
<div :style="{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: '80px' }">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.problem.heading }}</h2>
<div :style="{ display: 'flex', flexDirection: 'column', gap: '18px', paddingTop: '12px' }">
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '18px', lineHeight: 1.6, maxWidth: '620px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.problem.p1 }}</p>
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '18px', lineHeight: 1.6, maxWidth: '620px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.problem.p2 }}</p>
</div>
</div>
</LandingContainer>
</section>
</template>
@@ -0,0 +1,125 @@
<script setup lang="ts">
// Stylized customer-portal dashboard shown under the hero. Illustrative only —
// labels are intentionally Danish in both languages. Ported from
// landing-sections.jsx ProductMockup (light mode).
import { computed } from 'vue'
import { C } from '~/utils/landingTokens'
import { useTheme, useDark } from '~/composables/useLanding'
const t = useTheme()
const dark = useDark()
const m = computed(() => ({
bg: dark.value ? '#171715' : '#FFFFFF',
border: dark.value ? 'rgba(255,255,255,0.08)' : 'rgba(10,10,10,0.08)',
fg: dark.value ? C.bone : C.carbon,
muted: dark.value ? 'rgba(244,243,238,0.55)' : 'rgba(10,10,10,0.55)',
subtle: dark.value ? 'rgba(244,243,238,0.08)' : 'rgba(10,10,10,0.04)',
}))
const apps = [
{ name: 'mail', badge: '12', active: true },
{ name: 'drev' },
{ name: 'møder' },
{ name: 'chat', pill: '3' },
{ name: 'admin' },
]
const stats: [string, string, string][] = [
['Ulæste', '12', 'mail'],
['Møder i dag', '3', 'møder'],
['Delte filer', '47', 'drev'],
]
const recent: [string, string, string][] = [
['Lone Frederiksen', 'Tilbud — Q3 retainer', '09:42'],
['ops@stalwart.io', 'Sikkerhedsopdatering 1.12 udrullet', '08:30'],
['Mads Holm', 'Re: Onboarding for nye seniors', 'i går'],
]
</script>
<template>
<div :style="{
background: m.bg, border: `1px solid ${m.border}`, borderRadius: '8px',
boxShadow: dark ? '0 40px 80px rgba(0,0,0,0.5)' : '0 30px 80px rgba(10,10,10,0.08)',
overflow: 'hidden',
}">
<!-- Window chrome -->
<div :style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 18px', borderBottom: `1px solid ${m.border}` }">
<span :style="{ width: '10px', height: '10px', borderRadius: '999px', background: '#E23030' }" />
<span :style="{ width: '10px', height: '10px', borderRadius: '999px', background: '#E89A1F' }" />
<span :style="{ width: '10px', height: '10px', borderRadius: '999px', background: '#1F8A5B' }" />
<div :style="{ marginLeft: '16px', padding: '4px 12px', background: m.subtle, borderRadius: '4px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: m.muted }">app.dezky.com / dashboard</div>
</div>
<div :style="{ display: 'grid', gridTemplateColumns: '220px 1fr', minHeight: '460px' }">
<!-- Sidebar -->
<div :style="{ borderRight: `1px solid ${m.border}`, padding: '20px 0' }">
<div :style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '0 20px', marginBottom: '24px' }">
<BrandNodeMark :size="20" :fg="m.fg" :accent="t.signal" />
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', fontWeight: 600, color: m.fg }">dezky</span>
</div>
<div
v-for="(a, i) in apps" :key="a.name"
:style="{
padding: '9px 20px',
background: a.active ? m.subtle : 'transparent',
borderLeft: `2px solid ${a.active ? t.signal : 'transparent'}`,
fontFamily: '\'Inter\', sans-serif', fontSize: '13px', fontWeight: a.active ? 600 : 500,
color: a.active ? m.fg : m.muted,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}"
>
<span>{{ a.name }}</span>
<span v-if="a.badge" :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', color: m.muted }">{{ a.badge }}</span>
<span v-else-if="a.pill" :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', background: t.signal, padding: '1px 6px', borderRadius: '2px', color: C.carbon }">{{ a.pill }}</span>
</div>
<div :style="{ margin: '32px 20px 0', padding: '14px', borderRadius: '4px', background: m.subtle, fontFamily: '\'Inter\', sans-serif' }">
<div :style="{ fontSize: '10px', fontFamily: '\'JetBrains Mono\', monospace', color: m.muted, letterSpacing: '0.1em', textTransform: 'uppercase' }">lagring</div>
<div :style="{ fontSize: '16px', fontWeight: 600, color: m.fg, marginTop: '6px' }">184 GB / 500</div>
<div :style="{ height: '4px', background: m.border, borderRadius: '999px', marginTop: '8px', overflow: 'hidden' }">
<div :style="{ height: '100%', width: '37%', background: t.signal }" />
</div>
</div>
</div>
<!-- Main -->
<div :style="{ padding: '28px 32px' }">
<div :style="{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }">
<div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: m.muted, letterSpacing: '0.1em', textTransform: 'uppercase' }">indbakke</div>
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '28px', color: m.fg, marginTop: '6px', letterSpacing: '-0.02em' }">god morgen, anne</div>
</div>
<div :style="{ display: 'flex', gap: '8px' }">
<div :style="{ width: '32px', height: '32px', borderRadius: '999px', background: m.subtle, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: m.fg, fontWeight: 600 }">AB</div>
</div>
</div>
<div :style="{ marginTop: '24px', display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px' }">
<div v-for="(s, i) in stats" :key="i" :style="{ padding: '16px 18px', background: m.subtle, borderRadius: '4px' }">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', color: m.muted, letterSpacing: '0.1em', textTransform: 'uppercase' }">{{ s[2] }}</div>
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontSize: '28px', fontWeight: 600, color: m.fg, marginTop: '4px' }">{{ s[1] }}</div>
<div :style="{ fontSize: '12px', color: m.muted, marginTop: '2px' }">{{ s[0] }}</div>
</div>
</div>
<div :style="{ marginTop: '24px' }">
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', color: m.muted, letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '12px' }">seneste · indbakke</div>
<div
v-for="(r, i) in recent" :key="i"
:style="{
display: 'grid', gridTemplateColumns: '200px 1fr 60px',
padding: '12px 0', borderTop: i === 0 ? `1px solid ${m.border}` : 'none',
borderBottom: `1px solid ${m.border}`,
fontFamily: '\'Inter\', sans-serif', fontSize: '13px', alignItems: 'center',
}"
>
<span :style="{ fontWeight: 600, color: m.fg }">{{ r[0] }}</span>
<span :style="{ color: m.muted }">{{ r[1] }}</span>
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: m.muted, textAlign: 'right' }">{{ r[2] }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,28 @@
<script setup lang="ts">
// Monospace "NN — label" section header with an underline. Ported from
// landing-sections.jsx SectionLabel. Pass the full "01 — udfordringen" string.
import { useTheme } from '~/composables/useLanding'
const props = defineProps<{ label: string }>()
const t = useTheme()
const parts = computed(() => {
const [index, ...rest] = props.label.split(' — ')
return { index, text: rest.join(' — ') }
})
</script>
<template>
<div :style="{
display: 'flex', alignItems: 'center', gap: '14px',
paddingBottom: '24px', marginBottom: '56px',
borderBottom: `1px solid ${t.border}`,
fontFamily: '\'JetBrains Mono\', monospace',
fontSize: '11px', letterSpacing: '0.16em', textTransform: 'uppercase',
color: t.fgMuted,
}">
<span :style="{ color: t.fgDim }">{{ parts.index }}</span>
<span></span>
<span>{{ parts.text }}</span>
</div>
</template>
@@ -0,0 +1,64 @@
<script setup lang="ts">
// Section 04 — sovereignty. Inverted (carbon) panel with a heading/body column
// and a spec table. Ported from landing-sections.jsx Sovereignty.
import { computed } from 'vue'
import { C } from '~/utils/landingTokens'
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
// Borders/muted derive from whether the inverted foreground is bone (light
// page → carbon panel → bone text) or carbon (dark page → bone panel).
const onBone = computed(() => t.value.invertFg === C.bone)
const rule14 = computed(() => (onBone.value ? 'rgba(244,243,238,0.14)' : 'rgba(10,10,10,0.14)'))
const rule18 = computed(() => (onBone.value ? 'rgba(244,243,238,0.18)' : 'rgba(10,10,10,0.18)'))
const rule10 = computed(() => (onBone.value ? 'rgba(244,243,238,0.1)' : 'rgba(10,10,10,0.1)'))
const muted55 = computed(() => (onBone.value ? 'rgba(244,243,238,0.55)' : 'rgba(10,10,10,0.55)'))
const muted70 = computed(() => (onBone.value ? 'rgba(244,243,238,0.7)' : 'rgba(10,10,10,0.7)'))
const labelParts = computed(() => {
const [index, ...rest] = copy.value.sovereignty.label.split(' — ')
return { index, text: rest.join(' — ') }
})
</script>
<template>
<section id="sovereignty" :style="{ background: t.invert, color: t.invertFg, scrollMarginTop: '72px' }">
<LandingContainer pad="140px 64px">
<div :style="{
display: 'flex', alignItems: 'center', gap: '14px',
paddingBottom: '24px', marginBottom: '56px',
borderBottom: `1px solid ${rule14}`,
fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px',
letterSpacing: '0.16em', textTransform: 'uppercase', color: muted55,
}">
<span :style="{ opacity: 0.6 }">{{ labelParts.index }}</span>
<span></span>
<span>{{ labelParts.text }}</span>
</div>
<div :style="{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', gap: '96px' }">
<div>
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.invertFg }">{{ copy.sovereignty.heading }}</h2>
<div :style="{ marginTop: '36px', display: 'flex', flexDirection: 'column', gap: '18px', maxWidth: '540px' }">
<p v-for="(p, i) in copy.sovereignty.body" :key="i" :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '17px', lineHeight: 1.55, color: muted70, margin: 0, textWrap: 'pretty' }">{{ p }}</p>
</div>
</div>
<div :style="{ paddingTop: '8px' }">
<div
v-for="(row, i) in copy.sovereignty.checks" :key="i"
:style="{
display: 'grid', gridTemplateColumns: '1fr 1.4fr', padding: '20px 0',
borderTop: i === 0 ? `1px solid ${rule18}` : 'none',
borderBottom: `1px solid ${rule10}`,
fontFamily: '\'Inter\', sans-serif', fontSize: '14px', alignItems: 'baseline',
}"
>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: muted55, letterSpacing: '0.06em', textTransform: 'uppercase' }">{{ row[0] }}</div>
<div :style="{ color: t.invertFg, fontWeight: 500 }">{{ row[1] }}</div>
</div>
</div>
</div>
</LandingContainer>
</section>
</template>
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
// Section 07 — under the hood. Open-source component table.
// Ported from landing-sections.jsx Stack.
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
</script>
<template>
<section id="stack" :style="{ background: t.bg, color: t.fg, scrollMarginTop: '72px' }">
<LandingContainer pad="140px 64px">
<LandingSectionLabel :label="copy.stack.label" />
<div :style="{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', gap: '80px', alignItems: 'end', marginBottom: '56px' }">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.stack.heading }}</h2>
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '20px', lineHeight: 1.5, maxWidth: '480px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.stack.lede }}</p>
</div>
<div>
<div
v-for="(row, i) in copy.stack.rows" :key="i"
:style="{
display: 'grid', gridTemplateColumns: '1fr 1.4fr 0.8fr 1fr 40px',
gap: '24px', padding: '24px 0',
borderTop: i === 0 ? `1px solid ${t.borderStrong}` : 'none',
borderBottom: `1px solid ${t.border}`,
alignItems: 'baseline', fontFamily: '\'Inter\', sans-serif', fontSize: '15px',
}"
>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: t.fgDim, letterSpacing: '0.08em', textTransform: 'uppercase' }">{{ row[0] }}</div>
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontSize: '20px', fontWeight: 600, color: t.fg, letterSpacing: '-0.015em' }">{{ row[1] }}</div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', color: t.fg, padding: '4px 10px', background: t.bgAlt, borderRadius: '3px', display: 'inline-block', justifySelf: 'start' }">{{ row[2] }}</div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '12px', color: t.fgMuted }">{{ row[3] }}</div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '14px', color: t.fgDim, textAlign: 'right' }"></div>
</div>
</div>
</LandingContainer>
</section>
</template>
+38
View File
@@ -0,0 +1,38 @@
<script setup lang="ts">
// Section 02 — the suite. Five module cards in a bordered row.
// Ported from landing-sections.jsx Suite.
import { useTheme, useCopy } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
</script>
<template>
<section id="suite" :style="{ background: t.bgAlt, color: t.fg, borderTop: `1px solid ${t.border}`, borderBottom: `1px solid ${t.border}`, scrollMarginTop: '72px' }">
<LandingContainer pad="120px 64px">
<LandingSectionLabel :label="copy.suite.label" />
<div :style="{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', gap: '80px', alignItems: 'end', marginBottom: '64px' }">
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.suite.heading }}</h2>
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '20px', lineHeight: 1.5, maxWidth: '520px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.suite.lede }}</p>
</div>
<div :style="{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '0', border: `1px solid ${t.border}` }">
<div
v-for="(card, i) in copy.suite.cards" :key="i"
:style="{
padding: '32px 28px',
borderRight: i < 4 ? `1px solid ${t.border}` : 'none',
background: t.surface,
display: 'flex', flexDirection: 'column', gap: '20px',
minHeight: '280px',
}"
>
<LandingModuleGlyph :name="card.name" />
<div>
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '22px', color: t.fg, letterSpacing: '-0.02em' }">{{ card.name }}</div>
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10.5px', color: t.fgDim, marginTop: '6px', letterSpacing: '0.08em', textTransform: 'lowercase' }">{{ card.tag }}</div>
</div>
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '13.5px', lineHeight: 1.6, maxWidth: '260px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ card.desc }}</p>
</div>
</div>
</LandingContainer>
</section>
</template>
@@ -0,0 +1,43 @@
<script setup lang="ts">
// Section 06 — for partners (whitelabel). Pitch column + partner-brand demo.
// Ported from landing-sections.jsx Whitelabel (light mode).
import { computed } from 'vue'
import { useTheme, useCopy, useDark } from '~/composables/useLanding'
const t = useTheme()
const copy = useCopy()
const dark = useDark()
const sectionBg = computed(() => (dark.value ? '#1A1A17' : '#EFEDE3'))
const cardBg = computed(() => (dark.value ? '#0F0F0D' : '#FFFFFF'))
</script>
<template>
<section id="whitelabel" :style="{ background: sectionBg, color: t.fg, borderTop: `1px solid ${t.border}`, borderBottom: `1px solid ${t.border}`, scrollMarginTop: '72px' }">
<LandingContainer pad="120px 64px">
<LandingSectionLabel :label="copy.whitelabel.label" />
<div :style="{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '80px', alignItems: 'start' }">
<div>
<h2 :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: 'clamp(36px, 4.4vw, 64px)', letterSpacing: '-0.032em', lineHeight: 1.0, margin: 0, textWrap: 'balance', color: t.fg }">{{ copy.whitelabel.heading }}</h2>
<div :style="{ marginTop: '32px', maxWidth: '520px' }">
<p :style="{ fontFamily: '\'Inter\', sans-serif', fontSize: '20px', lineHeight: 1.5, maxWidth: '640px', color: t.fgMuted, margin: 0, textWrap: 'pretty' }">{{ copy.whitelabel.lede }}</p>
</div>
<div :style="{ marginTop: '32px', display: 'flex', flexDirection: 'column', gap: '12px' }">
<div v-for="(b, i) in copy.whitelabel.bullets" :key="i" :style="{ display: 'flex', alignItems: 'center', gap: '14px', fontFamily: '\'Inter\', sans-serif', fontSize: '15px', color: t.fg }">
<span :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', color: t.fgDim, width: '24px' }">0{{ i + 1 }}</span>
<span>{{ b }}</span>
</div>
</div>
<div :style="{ marginTop: '40px' }">
<LandingBtn variant="secondary" size="lg">{{ copy.whitelabel.cta }} </LandingBtn>
</div>
</div>
<div :style="{ display: 'flex', flexDirection: 'column', gap: '16px' }">
<LandingPartnerCard :fg="t.fg" :bg="cardBg" :border="t.border" accent="#D6502A" name="moltke it" subtitle="aalborg · 24 brugere" />
<LandingPartnerCard :fg="t.fg" :bg="cardBg" :border="t.border" accent="#3956C8" name="kraft & partners" subtitle="københavn · 112 brugere" />
<LandingPartnerCard :fg="t.fg" :bg="cardBg" :border="t.border" :accent="t.signal" name="dit firma her" subtitle="—" placeholder />
</div>
</div>
</LandingContainer>
</section>
</template>