create-clientkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/THIRD-PARTY.md +61 -0
- package/bin/cli.js +52 -0
- package/dist/cli.js +2556 -0
- package/package.json +84 -0
- package/templates/astro-tailwind/base/README.md +131 -0
- package/templates/astro-tailwind/base/_gitattributes +5 -0
- package/templates/astro-tailwind/base/_gitignore +23 -0
- package/templates/astro-tailwind/base/_package.json +28 -0
- package/templates/astro-tailwind/base/astro.config.mjs +32 -0
- package/templates/astro-tailwind/base/public/favicon.svg +5 -0
- package/templates/astro-tailwind/base/src/components/Brand.astro +89 -0
- package/templates/astro-tailwind/base/src/components/Footer.astro +88 -0
- package/templates/astro-tailwind/base/src/components/Header.astro +104 -0
- package/templates/astro-tailwind/base/src/components/LaunchNotice.astro +156 -0
- package/templates/astro-tailwind/base/src/components/Seo.astro +73 -0
- package/templates/astro-tailwind/base/src/components/SocialLinks.astro +75 -0
- package/templates/astro-tailwind/base/src/components/StructuredData.astro +39 -0
- package/templates/astro-tailwind/base/src/config/site.config.ts +141 -0
- package/templates/astro-tailwind/base/src/layouts/BaseLayout.astro +121 -0
- package/templates/astro-tailwind/base/src/lib/seo.ts +87 -0
- package/templates/astro-tailwind/base/src/pages/404.astro +147 -0
- package/templates/astro-tailwind/base/src/pages/index.astro +31 -0
- package/templates/astro-tailwind/base/src/pages/robots.txt.ts +32 -0
- package/templates/astro-tailwind/base/src/styles/global.css +330 -0
- package/templates/astro-tailwind/base/tsconfig.json +11 -0
- package/templates/astro-tailwind/modes/coming-soon/_package.json +3 -0
- package/templates/astro-tailwind/modes/coming-soon/src/pages/index.astro +149 -0
- package/templates/astro-tailwind/modes/full/_package.json +3 -0
- package/templates/astro-tailwind/modes/full/src/pages/index.astro +237 -0
- package/templates/astro-tailwind/template.json +23 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { CONTACT, NAV } from '../config/site.config.ts';
|
|
3
|
+
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
4
|
+
|
|
5
|
+
const hasEmail = CONTACT.email !== '';
|
|
6
|
+
const links = NAV.filter((item) => item.label !== '' && item.href !== '');
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<BaseLayout
|
|
10
|
+
title="Page not found"
|
|
11
|
+
description="This page could not be found."
|
|
12
|
+
noindex={true}
|
|
13
|
+
structuredData={false}
|
|
14
|
+
measure="narrow"
|
|
15
|
+
>
|
|
16
|
+
<div class="wrap">
|
|
17
|
+
<div class="container-page inner">
|
|
18
|
+
<p class="code rise" aria-hidden="true">404</p>
|
|
19
|
+
|
|
20
|
+
<h1 class="rise" style="animation-delay: 60ms">This page doesn't exist.</h1>
|
|
21
|
+
|
|
22
|
+
<p class="lead rise" style="animation-delay: 120ms">
|
|
23
|
+
The link may be out of date, or the address may have been mistyped.
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<div class="actions rise" style="animation-delay: 180ms">
|
|
27
|
+
<a class="btn btn-primary" href="/">Return home</a>
|
|
28
|
+
{
|
|
29
|
+
hasEmail && (
|
|
30
|
+
<a class="btn btn-secondary" href={`mailto:${CONTACT.email}`}>
|
|
31
|
+
Report a broken link
|
|
32
|
+
</a>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
links.length > 0 && (
|
|
39
|
+
<nav class="suggestions rise" style="animation-delay: 240ms" aria-label="Other pages">
|
|
40
|
+
<p class="eyebrow">Try instead</p>
|
|
41
|
+
<ul>
|
|
42
|
+
{links.map((item) => (
|
|
43
|
+
<li>
|
|
44
|
+
<a class="link" href={item.href}>
|
|
45
|
+
{item.label}
|
|
46
|
+
</a>
|
|
47
|
+
</li>
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
</nav>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</BaseLayout>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.wrap {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
min-height: calc(100dvh - 10rem);
|
|
62
|
+
padding-block: var(--spacing-section);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.inner {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
align-items: flex-start;
|
|
69
|
+
gap: 1.25rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Oversized, quiet, and decorative - the real heading below carries the
|
|
73
|
+
meaning, so this is hidden from assistive technology. */
|
|
74
|
+
.code {
|
|
75
|
+
margin: 0 0 -0.5rem;
|
|
76
|
+
font-size: clamp(4.5rem, 2rem + 11vw, 9rem);
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
line-height: 0.9;
|
|
79
|
+
letter-spacing: -0.05em;
|
|
80
|
+
color: transparent;
|
|
81
|
+
-webkit-text-stroke: 1.5px var(--color-border-strong);
|
|
82
|
+
user-select: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
h1 {
|
|
86
|
+
margin: 0;
|
|
87
|
+
font-size: var(--text-h1);
|
|
88
|
+
line-height: var(--text-h1--line-height);
|
|
89
|
+
letter-spacing: var(--text-h1--letter-spacing);
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
text-wrap: balance;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.lead {
|
|
95
|
+
margin: 0;
|
|
96
|
+
max-width: 48ch;
|
|
97
|
+
color: var(--color-muted);
|
|
98
|
+
font-size: var(--text-lead);
|
|
99
|
+
line-height: var(--text-lead--line-height);
|
|
100
|
+
text-wrap: pretty;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.actions {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
gap: 0.75rem;
|
|
107
|
+
margin-top: 0.5rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.suggestions {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
gap: 0.75rem;
|
|
114
|
+
margin-top: 1.5rem;
|
|
115
|
+
padding-top: 1.5rem;
|
|
116
|
+
border-top: 1px solid var(--color-border);
|
|
117
|
+
width: 100%;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.suggestions ul {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-wrap: wrap;
|
|
123
|
+
gap: 0.5rem 1.5rem;
|
|
124
|
+
margin: 0;
|
|
125
|
+
padding: 0;
|
|
126
|
+
list-style: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.suggestions a {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
min-height: 2.25rem;
|
|
133
|
+
font-size: var(--text-small);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@media (max-width: 30rem) {
|
|
137
|
+
.actions {
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
align-items: stretch;
|
|
140
|
+
width: 100%;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.actions .btn {
|
|
144
|
+
width: 100%;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<BaseLayout>
|
|
6
|
+
<div class="container-page wrap">
|
|
7
|
+
<h1>Placeholder</h1>
|
|
8
|
+
<p>
|
|
9
|
+
This page comes from the template's base layer and is replaced by the selected mode. If you
|
|
10
|
+
are seeing it, the mode layer did not apply.
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
</BaseLayout>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
.wrap {
|
|
17
|
+
padding-block: var(--spacing-section);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
h1 {
|
|
21
|
+
margin: 0 0 1rem;
|
|
22
|
+
font-size: var(--text-h1);
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
p {
|
|
27
|
+
margin: 0;
|
|
28
|
+
max-width: 60ch;
|
|
29
|
+
color: var(--color-muted);
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
import { SEO, SITE } from '../config/site.config.ts';
|
|
4
|
+
import { absoluteUrl, siteOrigin } from '../lib/seo.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* robots.txt is generated at build time rather than dropped in public/, so it
|
|
8
|
+
* always reflects the current site.config.ts instead of whatever was true when
|
|
9
|
+
* the project was scaffolded.
|
|
10
|
+
*
|
|
11
|
+
* The sitemap is only advertised when a production URL exists AND the site is
|
|
12
|
+
* indexable - the same condition that decides whether a sitemap is built at
|
|
13
|
+
* all (see astro.config.mjs). A URL-less site gets a valid, permissive
|
|
14
|
+
* robots.txt with no sitemap line, never a fabricated domain.
|
|
15
|
+
*/
|
|
16
|
+
export const GET: APIRoute = () => {
|
|
17
|
+
const origin = siteOrigin(SITE.url);
|
|
18
|
+
const lines = ['User-agent: *'];
|
|
19
|
+
|
|
20
|
+
if (SEO.noindex) {
|
|
21
|
+
lines.push('Disallow: /');
|
|
22
|
+
} else {
|
|
23
|
+
lines.push('Allow: /');
|
|
24
|
+
if (origin !== '') {
|
|
25
|
+
lines.push('', `Sitemap: ${absoluteUrl(origin, '/sitemap-index.xml')}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new Response(`${lines.join('\n')}\n`, {
|
|
30
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
/* ===========================================================================
|
|
4
|
+
1. Palette
|
|
5
|
+
---------------------------------------------------------------------------
|
|
6
|
+
Raw colour values live here as ordinary custom properties, and the Tailwind
|
|
7
|
+
theme below only points at them. That means the entire site re-themes from
|
|
8
|
+
this one block - light, dark and brand accent - without a single component
|
|
9
|
+
being duplicated or a line of JavaScript being shipped.
|
|
10
|
+
|
|
11
|
+
Neutral by design: the default look is ink on paper so it suits any client.
|
|
12
|
+
Set `theme.accent` in src/config/site.config.ts to introduce brand colour.
|
|
13
|
+
=========================================================================== */
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
color-scheme: light;
|
|
17
|
+
|
|
18
|
+
--ck-background: #ffffff;
|
|
19
|
+
--ck-surface: #f7f8f9;
|
|
20
|
+
--ck-surface-strong: #eef0f2;
|
|
21
|
+
--ck-foreground: #0f1216;
|
|
22
|
+
--ck-muted: #5a616d;
|
|
23
|
+
--ck-border: #e4e6ea;
|
|
24
|
+
--ck-border-strong: #cfd3da;
|
|
25
|
+
|
|
26
|
+
/* Overridden per-site by BaseLayout when theme.accent is set. */
|
|
27
|
+
--ck-accent: #0f1216;
|
|
28
|
+
--ck-accent-foreground: #ffffff;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@media (prefers-color-scheme: dark) {
|
|
32
|
+
:root:not([data-appearance='light']) {
|
|
33
|
+
color-scheme: dark;
|
|
34
|
+
|
|
35
|
+
--ck-background: #0b0d10;
|
|
36
|
+
--ck-surface: #12151a;
|
|
37
|
+
--ck-surface-strong: #1a1f26;
|
|
38
|
+
--ck-foreground: #eceef1;
|
|
39
|
+
--ck-muted: #9aa2b1;
|
|
40
|
+
--ck-border: #232830;
|
|
41
|
+
--ck-border-strong: #333a45;
|
|
42
|
+
|
|
43
|
+
--ck-accent: #eceef1;
|
|
44
|
+
--ck-accent-foreground: #0b0d10;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:root[data-appearance='dark'] {
|
|
49
|
+
color-scheme: dark;
|
|
50
|
+
|
|
51
|
+
--ck-background: #0b0d10;
|
|
52
|
+
--ck-surface: #12151a;
|
|
53
|
+
--ck-surface-strong: #1a1f26;
|
|
54
|
+
--ck-foreground: #eceef1;
|
|
55
|
+
--ck-muted: #9aa2b1;
|
|
56
|
+
--ck-border: #232830;
|
|
57
|
+
--ck-border-strong: #333a45;
|
|
58
|
+
|
|
59
|
+
--ck-accent: #eceef1;
|
|
60
|
+
--ck-accent-foreground: #0b0d10;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ===========================================================================
|
|
64
|
+
2. Design tokens
|
|
65
|
+
---------------------------------------------------------------------------
|
|
66
|
+
Tailwind v4 is configured in CSS. There is no tailwind.config.js.
|
|
67
|
+
Everything declared here becomes a utility: `bg-surface`, `text-muted`,
|
|
68
|
+
`text-display`, `rounded-lg`, `shadow-lifted`, and so on.
|
|
69
|
+
=========================================================================== */
|
|
70
|
+
|
|
71
|
+
@theme {
|
|
72
|
+
--font-sans:
|
|
73
|
+
ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
|
74
|
+
'Noto Sans', sans-serif;
|
|
75
|
+
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
76
|
+
|
|
77
|
+
/* Semantic colours - components never reference a raw hex. */
|
|
78
|
+
--color-background: var(--ck-background);
|
|
79
|
+
--color-surface: var(--ck-surface);
|
|
80
|
+
--color-surface-strong: var(--ck-surface-strong);
|
|
81
|
+
--color-foreground: var(--ck-foreground);
|
|
82
|
+
--color-muted: var(--ck-muted);
|
|
83
|
+
--color-border: var(--ck-border);
|
|
84
|
+
--color-border-strong: var(--ck-border-strong);
|
|
85
|
+
--color-accent: var(--ck-accent);
|
|
86
|
+
--color-accent-foreground: var(--ck-accent-foreground);
|
|
87
|
+
|
|
88
|
+
/* Type scale. Display and h1 are fluid so long client names stay readable
|
|
89
|
+
from 320px upwards without a pile of breakpoint overrides. */
|
|
90
|
+
--text-display: clamp(2.5rem, 1.35rem + 5.1vw, 4.75rem);
|
|
91
|
+
--text-display--line-height: 1.03;
|
|
92
|
+
--text-display--letter-spacing: -0.035em;
|
|
93
|
+
|
|
94
|
+
--text-h1: clamp(2rem, 1.35rem + 2.9vw, 3.25rem);
|
|
95
|
+
--text-h1--line-height: 1.08;
|
|
96
|
+
--text-h1--letter-spacing: -0.03em;
|
|
97
|
+
|
|
98
|
+
--text-h2: clamp(1.5rem, 1.25rem + 1.1vw, 2rem);
|
|
99
|
+
--text-h2--line-height: 1.18;
|
|
100
|
+
--text-h2--letter-spacing: -0.02em;
|
|
101
|
+
|
|
102
|
+
--text-h3: 1.125rem;
|
|
103
|
+
--text-h3--line-height: 1.4;
|
|
104
|
+
--text-h3--letter-spacing: -0.01em;
|
|
105
|
+
|
|
106
|
+
--text-lead: clamp(1.0625rem, 1rem + 0.35vw, 1.25rem);
|
|
107
|
+
--text-lead--line-height: 1.6;
|
|
108
|
+
|
|
109
|
+
--text-body: 1rem;
|
|
110
|
+
--text-body--line-height: 1.65;
|
|
111
|
+
|
|
112
|
+
--text-small: 0.875rem;
|
|
113
|
+
--text-small--line-height: 1.55;
|
|
114
|
+
|
|
115
|
+
--text-label: 0.75rem;
|
|
116
|
+
--text-label--line-height: 1.2;
|
|
117
|
+
--text-label--letter-spacing: 0.14em;
|
|
118
|
+
|
|
119
|
+
/* Restrained radii - nothing is a pill except deliberate controls. */
|
|
120
|
+
--radius-sm: 0.375rem;
|
|
121
|
+
--radius-md: 0.5rem;
|
|
122
|
+
--radius-lg: 0.75rem;
|
|
123
|
+
--radius-xl: 1rem;
|
|
124
|
+
|
|
125
|
+
/* Two shadows only, both subtle. Depth is used for hierarchy, not decoration. */
|
|
126
|
+
--shadow-subtle: 0 1px 2px 0 rgb(15 18 22 / 0.04), 0 1px 3px 0 rgb(15 18 22 / 0.06);
|
|
127
|
+
--shadow-lifted: 0 4px 12px -2px rgb(15 18 22 / 0.08), 0 12px 32px -8px rgb(15 18 22 / 0.1);
|
|
128
|
+
|
|
129
|
+
/* Vertical rhythm between major page blocks. */
|
|
130
|
+
--spacing-section: 4.5rem;
|
|
131
|
+
--spacing-section-lg: 7rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ===========================================================================
|
|
135
|
+
3. Base
|
|
136
|
+
=========================================================================== */
|
|
137
|
+
|
|
138
|
+
@layer base {
|
|
139
|
+
html {
|
|
140
|
+
-webkit-text-size-adjust: 100%;
|
|
141
|
+
scroll-behavior: smooth;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
body {
|
|
145
|
+
background-color: var(--color-background);
|
|
146
|
+
color: var(--color-foreground);
|
|
147
|
+
font-family: var(--font-sans);
|
|
148
|
+
font-size: var(--text-body);
|
|
149
|
+
line-height: 1.65;
|
|
150
|
+
-webkit-font-smoothing: antialiased;
|
|
151
|
+
text-rendering: optimizeLegibility;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* One consistent focus treatment everywhere, always visible. */
|
|
155
|
+
:focus-visible {
|
|
156
|
+
outline: 2px solid var(--color-accent);
|
|
157
|
+
outline-offset: 3px;
|
|
158
|
+
border-radius: 2px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
::selection {
|
|
162
|
+
background-color: var(--color-accent);
|
|
163
|
+
color: var(--color-accent-foreground);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Long client names and URLs should wrap, never overflow the viewport. */
|
|
167
|
+
h1,
|
|
168
|
+
h2,
|
|
169
|
+
h3,
|
|
170
|
+
p {
|
|
171
|
+
overflow-wrap: break-word;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ===========================================================================
|
|
176
|
+
4. Components
|
|
177
|
+
---------------------------------------------------------------------------
|
|
178
|
+
Written as plain CSS against the tokens above so restyling a button is one
|
|
179
|
+
edit here rather than a search across every page.
|
|
180
|
+
=========================================================================== */
|
|
181
|
+
|
|
182
|
+
@layer components {
|
|
183
|
+
.page-shell {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
min-height: 100dvh;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.container-page {
|
|
190
|
+
width: 100%;
|
|
191
|
+
/* Set by BaseLayout's `measure` prop, so header, content and footer all
|
|
192
|
+
share one column width instead of each choosing its own. */
|
|
193
|
+
max-width: var(--ck-measure, 72rem);
|
|
194
|
+
margin-inline: auto;
|
|
195
|
+
padding-inline: 1.25rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@media (min-width: 640px) {
|
|
199
|
+
.container-page {
|
|
200
|
+
padding-inline: 2rem;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Buttons ---------------------------------------------------------------- */
|
|
205
|
+
|
|
206
|
+
.btn {
|
|
207
|
+
display: inline-flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
gap: 0.5rem;
|
|
211
|
+
min-height: 2.75rem; /* comfortable touch target */
|
|
212
|
+
padding: 0.625rem 1.25rem;
|
|
213
|
+
border: 1px solid transparent;
|
|
214
|
+
border-radius: var(--radius-md);
|
|
215
|
+
font-size: var(--text-small);
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
line-height: 1;
|
|
218
|
+
text-decoration: none;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
transition:
|
|
221
|
+
background-color 160ms ease,
|
|
222
|
+
border-color 160ms ease,
|
|
223
|
+
color 160ms ease,
|
|
224
|
+
transform 160ms ease;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.btn:active {
|
|
228
|
+
transform: translateY(1px);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.btn-primary {
|
|
232
|
+
background-color: var(--color-accent);
|
|
233
|
+
color: var(--color-accent-foreground);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.btn-primary:hover {
|
|
237
|
+
background-color: color-mix(in oklab, var(--color-accent) 86%, var(--color-background));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.btn-secondary {
|
|
241
|
+
background-color: transparent;
|
|
242
|
+
border-color: var(--color-border-strong);
|
|
243
|
+
color: var(--color-foreground);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.btn-secondary:hover {
|
|
247
|
+
background-color: var(--color-surface);
|
|
248
|
+
border-color: var(--color-foreground);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* Eyebrow ---------------------------------------------------------------- */
|
|
252
|
+
|
|
253
|
+
.eyebrow {
|
|
254
|
+
display: inline-flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
gap: 0.625rem;
|
|
257
|
+
font-size: var(--text-label);
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
letter-spacing: var(--text-label--letter-spacing);
|
|
260
|
+
text-transform: uppercase;
|
|
261
|
+
color: var(--color-muted);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.eyebrow::before {
|
|
265
|
+
content: '';
|
|
266
|
+
width: 1.75rem;
|
|
267
|
+
height: 1px;
|
|
268
|
+
background-color: var(--color-border-strong);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Inline text links ------------------------------------------------------ */
|
|
272
|
+
|
|
273
|
+
.link {
|
|
274
|
+
color: var(--color-foreground);
|
|
275
|
+
text-decoration: underline;
|
|
276
|
+
text-decoration-color: var(--color-border-strong);
|
|
277
|
+
text-underline-offset: 0.25em;
|
|
278
|
+
transition: text-decoration-color 160ms ease;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.link:hover {
|
|
282
|
+
text-decoration-color: var(--color-accent);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Hairline rule ---------------------------------------------------------- */
|
|
286
|
+
|
|
287
|
+
.rule {
|
|
288
|
+
border: 0;
|
|
289
|
+
border-top: 1px solid var(--color-border);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ===========================================================================
|
|
294
|
+
5. Motion
|
|
295
|
+
---------------------------------------------------------------------------
|
|
296
|
+
Entrance animation is applied only when the visitor has not asked for
|
|
297
|
+
reduced motion, so the default resting state is fully visible. Nothing is
|
|
298
|
+
ever hidden behind an animation that might not run.
|
|
299
|
+
=========================================================================== */
|
|
300
|
+
|
|
301
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
302
|
+
.rise {
|
|
303
|
+
animation: ck-rise 640ms cubic-bezier(0.22, 0.68, 0.24, 1) both;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@keyframes ck-rise {
|
|
307
|
+
from {
|
|
308
|
+
opacity: 0;
|
|
309
|
+
transform: translateY(0.625rem);
|
|
310
|
+
}
|
|
311
|
+
to {
|
|
312
|
+
opacity: 1;
|
|
313
|
+
transform: none;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@media (prefers-reduced-motion: reduce) {
|
|
319
|
+
html {
|
|
320
|
+
scroll-behavior: auto;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
*,
|
|
324
|
+
*::before,
|
|
325
|
+
*::after {
|
|
326
|
+
animation-duration: 0.01ms !important;
|
|
327
|
+
animation-iteration-count: 1 !important;
|
|
328
|
+
transition-duration: 0.01ms !important;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "astro/tsconfigs/strict",
|
|
3
|
+
"include": [".astro/types.d.ts", "**/*"],
|
|
4
|
+
"exclude": ["dist"],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"strictNullChecks": true,
|
|
7
|
+
"noUnusedLocals": true,
|
|
8
|
+
"noUnusedParameters": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Brand from '../components/Brand.astro';
|
|
3
|
+
import LaunchNotice from '../components/LaunchNotice.astro';
|
|
4
|
+
import SocialLinks from '../components/SocialLinks.astro';
|
|
5
|
+
import { CONTACT, LAUNCH, SITE, SOCIAL } from '../config/site.config.ts';
|
|
6
|
+
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
7
|
+
|
|
8
|
+
const hasEmail = CONTACT.email !== '';
|
|
9
|
+
const hasSocial = SOCIAL.some((link) => link.label !== '' && link.href !== '');
|
|
10
|
+
const hasLaunch = LAUNCH.enabled && LAUNCH.date !== '';
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
{/* The page carries its own brand lockup, so a header would only repeat it. */}
|
|
14
|
+
<BaseLayout showHeader={false} measure="narrow">
|
|
15
|
+
<div class="hero">
|
|
16
|
+
<div class="glow" aria-hidden="true"></div>
|
|
17
|
+
|
|
18
|
+
<div class="container-page inner">
|
|
19
|
+
<div class="brand rise">
|
|
20
|
+
<Brand size="lg" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<p class="eyebrow rise" style="animation-delay: 60ms">Coming soon</p>
|
|
24
|
+
|
|
25
|
+
<h1 class="rise" style="animation-delay: 120ms">{SITE.name}</h1>
|
|
26
|
+
|
|
27
|
+
<p class="lead rise" style="animation-delay: 180ms">{SITE.description}</p>
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
hasLaunch && (
|
|
31
|
+
<div class="rise" style="animation-delay: 240ms">
|
|
32
|
+
<LaunchNotice />
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
(hasEmail || hasSocial) && (
|
|
39
|
+
<div class="actions rise" style="animation-delay: 300ms">
|
|
40
|
+
{hasEmail && (
|
|
41
|
+
<a class="btn btn-primary" href={`mailto:${CONTACT.email}`}>
|
|
42
|
+
Get in touch
|
|
43
|
+
</a>
|
|
44
|
+
)}
|
|
45
|
+
{hasSocial && <SocialLinks label="Social links" />}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
CONTACT.location !== '' && (
|
|
52
|
+
<p class="location rise" style="animation-delay: 340ms">
|
|
53
|
+
{CONTACT.location}
|
|
54
|
+
</p>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</BaseLayout>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.hero {
|
|
63
|
+
position: relative;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
min-height: calc(100dvh - 6rem);
|
|
67
|
+
padding-block: var(--spacing-section);
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* One soft, very low-contrast wash tinted by the accent. No blobs and no
|
|
72
|
+
page-wide gradient - just enough that the page does not read as a blank
|
|
73
|
+
sheet of paper. */
|
|
74
|
+
.glow {
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset-inline: 0;
|
|
77
|
+
top: -30vh;
|
|
78
|
+
height: 70vh;
|
|
79
|
+
background: radial-gradient(
|
|
80
|
+
55% 60% at 50% 50%,
|
|
81
|
+
color-mix(in oklab, var(--color-accent) 8%, transparent),
|
|
82
|
+
transparent 70%
|
|
83
|
+
);
|
|
84
|
+
pointer-events: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.inner {
|
|
88
|
+
position: relative;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: flex-start;
|
|
92
|
+
gap: 1.5rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.brand {
|
|
96
|
+
margin-bottom: 0.5rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
h1 {
|
|
100
|
+
margin: 0;
|
|
101
|
+
max-width: 16ch;
|
|
102
|
+
font-size: var(--text-display);
|
|
103
|
+
line-height: var(--text-display--line-height);
|
|
104
|
+
letter-spacing: var(--text-display--letter-spacing);
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
text-wrap: balance;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.lead {
|
|
110
|
+
margin: 0;
|
|
111
|
+
max-width: 52ch;
|
|
112
|
+
color: var(--color-muted);
|
|
113
|
+
font-size: var(--text-lead);
|
|
114
|
+
line-height: var(--text-lead--line-height);
|
|
115
|
+
text-wrap: pretty;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.actions {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 1rem 1.75rem;
|
|
123
|
+
margin-top: 0.5rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.location {
|
|
127
|
+
margin: 0;
|
|
128
|
+
color: var(--color-muted);
|
|
129
|
+
font-size: var(--text-small);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* On the narrowest screens the call to action goes full width so it is an
|
|
133
|
+
easy target, and the headline gets the full measure. */
|
|
134
|
+
@media (max-width: 30rem) {
|
|
135
|
+
.actions {
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
align-items: stretch;
|
|
138
|
+
width: 100%;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.actions :global(.btn) {
|
|
142
|
+
width: 100%;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
h1 {
|
|
146
|
+
max-width: none;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</style>
|