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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user