@jtakeit/astro 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 +59 -0
- package/bin/jtk.mjs +41 -0
- package/docs/booking.md +164 -0
- package/docs/catalogue.md +459 -0
- package/docs/collections.md +249 -0
- package/docs/css.md +86 -0
- package/docs/gallery.md +127 -0
- package/docs/hero-motion.md +189 -0
- package/docs/kit.md +454 -0
- package/docs/languages.md +182 -0
- package/docs/lead-form.md +109 -0
- package/docs/pages.md +193 -0
- package/docs/photos.md +314 -0
- package/docs/scaffold.md +75 -0
- package/docs/shapes.md +140 -0
- package/docs/surface.md +187 -0
- package/lib/catalogue.mjs +1678 -0
- package/lib/codes.mjs +171 -0
- package/lib/create.mjs +282 -0
- package/package.json +16 -0
- package/template/astro.config.mjs +84 -0
- package/template/figures.mjs +122 -0
- package/template/gitignore +16 -0
- package/template/jtakeit-meta.mjs +112 -0
- package/template/jtk/content/index.json +38 -0
- package/template/jtk/design.json +24 -0
- package/template/markdown.mjs +36 -0
- package/template/package-lock.json +5320 -0
- package/template/package.json +26 -0
- package/template/specimens.mjs +46 -0
- package/template/src/components/Blocks.astro +151 -0
- package/template/src/components/BookingForm.astro +506 -0
- package/template/src/components/Clip.astro +155 -0
- package/template/src/components/Hero.astro +66 -0
- package/template/src/components/LeadForm.astro +347 -0
- package/template/src/components/OpeningHours.astro +69 -0
- package/template/src/components/Pile.astro +185 -0
- package/template/src/components/Shot.astro +472 -0
- package/template/src/components/gallery/Gallery.astro +381 -0
- package/template/src/components/gallery/galleries.ts +139 -0
- package/template/src/components/motion/HeroField.astro +520 -0
- package/template/src/components/motion/fields.ts +430 -0
- package/template/src/components/surface/Pattern.astro +278 -0
- package/template/src/components/surface/patterns.ts +187 -0
- package/template/src/content/blocks.ts +758 -0
- package/template/src/content.config.ts +19 -0
- package/template/src/copy/LOCALE.ts +324 -0
- package/template/src/data/site.ts +137 -0
- package/template/src/layouts/Layout.astro +282 -0
- package/template/src/lib/alive.ts +49 -0
- package/template/src/lib/entries.ts +106 -0
- package/template/src/lib/entryLoader.ts +315 -0
- package/template/src/lib/noise.ts +26 -0
- package/template/src/lib/page.ts +287 -0
- package/template/src/lib/photos.ts +168 -0
- package/template/src/lib/under.ts +32 -0
- package/template/src/lib/uploads.ts +85 -0
- package/template/src/pages/[...entry].astro +207 -0
- package/template/src/pages/[...feed].xml.ts +64 -0
- package/template/src/pages/index.astro +90 -0
- package/template/src/pages/llms.txt.ts +50 -0
- package/template/src/pages/privacy.astro +59 -0
- package/template/src/pages/robots.txt.ts +21 -0
- package/template/src/pages/sitemap.xml.ts +50 -0
- package/template/src/styles/global.css +411 -0
- package/template/src/styles/surface.css +375 -0
- package/template/tsconfig.json +5 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { BUSINESS, INDEXABLE, PAGES, fullAddress } from '../data/site';
|
|
3
|
+
import { allListed, href } from '../lib/entries';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A plain-text summary of the business for a language model that lands on the
|
|
7
|
+
* site. No engine promises to read it; it costs one route, and for a client
|
|
8
|
+
* whose pitch is «be findable» it is a reasonable thing to ship.
|
|
9
|
+
*
|
|
10
|
+
* Built from the same data as the page and the structured data, so it cannot
|
|
11
|
+
* contradict either. Empty while INDEXABLE is false — a preview should not be
|
|
12
|
+
* summarising itself to anyone.
|
|
13
|
+
*/
|
|
14
|
+
export const GET: APIRoute = async () => {
|
|
15
|
+
if (!INDEXABLE) {
|
|
16
|
+
return new Response('', { headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// What the owner writes, listed under its own heading. An assistant that
|
|
20
|
+
// lands here is looking for what this business has said, and a title with an
|
|
21
|
+
// address is the shortest honest form of it.
|
|
22
|
+
const collections = await allListed();
|
|
23
|
+
|
|
24
|
+
const lines = [
|
|
25
|
+
`# ${BUSINESS.name}`,
|
|
26
|
+
'',
|
|
27
|
+
`> ${BUSINESS.description}`,
|
|
28
|
+
'',
|
|
29
|
+
fullAddress,
|
|
30
|
+
BUSINESS.phoneDisplay,
|
|
31
|
+
BUSINESS.email,
|
|
32
|
+
BUSINESS.priceRange,
|
|
33
|
+
'',
|
|
34
|
+
'## Pages',
|
|
35
|
+
...PAGES.map((p) => `- ${p.path}: ${p.summary}`),
|
|
36
|
+
...collections.flatMap(({ collection, entries }) =>
|
|
37
|
+
entries.length === 0
|
|
38
|
+
? []
|
|
39
|
+
: [
|
|
40
|
+
'',
|
|
41
|
+
`## ${collection.label}`,
|
|
42
|
+
...entries.map((entry) => `- ${href(collection, entry)}: ${String(entry.data.title ?? '')}`),
|
|
43
|
+
],
|
|
44
|
+
),
|
|
45
|
+
].filter((line) => line !== undefined && line !== null);
|
|
46
|
+
|
|
47
|
+
return new Response(`${lines.join('\n')}\n`, {
|
|
48
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A privacy page, as a starting point.
|
|
4
|
+
*
|
|
5
|
+
* ── what the platform requires, which is one line of this file ──────────────
|
|
6
|
+
*
|
|
7
|
+
* **The link, and nothing else.** The platform walks the built artifact looking
|
|
8
|
+
* for `PROCESSING_URL`, and it does not read a word around it. Everything else
|
|
9
|
+
* on this page — the sections, the wording, whether this file is used at all —
|
|
10
|
+
* belongs to the business, and grading it is neither a website builder's job
|
|
11
|
+
* nor within its competence.
|
|
12
|
+
*
|
|
13
|
+
* So this is a default and not a specification. Rewrite it, restyle it, replace
|
|
14
|
+
* it with a page written by somebody's lawyer, or put the link on a privacy
|
|
15
|
+
* page that already exists and delete this file. All of those are correct.
|
|
16
|
+
*
|
|
17
|
+
* What is not correct is dropping the link: it is the one sentence here that is
|
|
18
|
+
* about somebody other than the business, and without it nothing on the site
|
|
19
|
+
* tells a visitor who is holding what they typed.
|
|
20
|
+
*
|
|
21
|
+
* ── and it is not a wall ────────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* A site missing this is **warned about and launched anyway**. Publishing a
|
|
24
|
+
* notice is the controller's obligation — the business's — and what makes the
|
|
25
|
+
* platform's own handling lawful is the agreement with them rather than a page
|
|
26
|
+
* here. See wiki/34: gate on our own conduct, never on somebody else's
|
|
27
|
+
* compliance. The one thing publishing does refuse is a form with nowhere to
|
|
28
|
+
* send what people write in it, which is a broken product rather than a legal
|
|
29
|
+
* opinion.
|
|
30
|
+
*/
|
|
31
|
+
import Layout from '../layouts/Layout.astro';
|
|
32
|
+
import { PRIVACY } from '../copy/{{LOCALE}}';
|
|
33
|
+
import { PROCESSING_URL } from '../content/blocks';
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<Layout title={PRIVACY.title} description={PRIVACY.intro}>
|
|
37
|
+
<main class="prose" data-jtk-fixed>
|
|
38
|
+
<h1>{PRIVACY.title}</h1>
|
|
39
|
+
<p class="lead">{PRIVACY.intro}</p>
|
|
40
|
+
|
|
41
|
+
<h2>{PRIVACY.formTitle}</h2>
|
|
42
|
+
<p>{PRIVACY.formBody}</p>
|
|
43
|
+
|
|
44
|
+
<h2>{PRIVACY.whoTitle}</h2>
|
|
45
|
+
<p>
|
|
46
|
+
{PRIVACY.whoBody}{' '}
|
|
47
|
+
<a href={PROCESSING_URL} rel="noreferrer">{PROCESSING_URL.replace('https://', '')}</a>
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<h2>{PRIVACY.askTitle}</h2>
|
|
51
|
+
<p>{PRIVACY.askBody}</p>
|
|
52
|
+
|
|
53
|
+
{/*
|
|
54
|
+
The business's own. Left visible rather than commented out, so that an
|
|
55
|
+
empty one is noticed before launch rather than after.
|
|
56
|
+
*/}
|
|
57
|
+
<h2>{PRIVACY.ownTitle}</h2>
|
|
58
|
+
</main>
|
|
59
|
+
</Layout>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { INDEXABLE, canonicalFor } from '../data/site';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Driven by the same switch as the robots meta tag, the sitemap and llms.txt —
|
|
6
|
+
* see src/data/site.ts. One flag, because a site that ships still telling
|
|
7
|
+
* Google to go away is always a site where two of the four were flipped.
|
|
8
|
+
*
|
|
9
|
+
* AI crawlers are deliberately not excluded. Note that Cloudflare's *managed*
|
|
10
|
+
* robots.txt, if it is on for the zone, blocks them regardless of this file —
|
|
11
|
+
* check the zone setting too.
|
|
12
|
+
*/
|
|
13
|
+
export const GET: APIRoute = () => {
|
|
14
|
+
const body = INDEXABLE
|
|
15
|
+
? ['User-agent: *', 'Allow: /', '', `Sitemap: ${canonicalFor('/sitemap.xml')}`, '']
|
|
16
|
+
: ['User-agent: *', 'Disallow: /', ''];
|
|
17
|
+
|
|
18
|
+
return new Response(body.join('\n'), {
|
|
19
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { INDEXABLE, PAGES, canonicalFor } from '../data/site';
|
|
3
|
+
import { allListed, href } from '../lib/entries';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The pages are written by hand in `PAGES` rather than generated, so that
|
|
7
|
+
* adding a page and forgetting to list it is a visible omission in one file
|
|
8
|
+
* instead of a silent one across the build.
|
|
9
|
+
*
|
|
10
|
+
* The entries are not, and cannot be: nobody writes them here — the owner makes
|
|
11
|
+
* them in the admin — so they are read from the collections, which is the only
|
|
12
|
+
* list of them that exists. A hidden entry is absent, because `allListed` is
|
|
13
|
+
* how anything here enumerates them.
|
|
14
|
+
*
|
|
15
|
+
* With INDEXABLE off the file is still served, but empty — an empty sitemap
|
|
16
|
+
* says «nothing to crawl», where a 404 says «try again later».
|
|
17
|
+
*/
|
|
18
|
+
export const GET: APIRoute = async () => {
|
|
19
|
+
const urls: { loc: string; priority: number }[] = [];
|
|
20
|
+
|
|
21
|
+
if (INDEXABLE) {
|
|
22
|
+
for (const page of PAGES) {
|
|
23
|
+
urls.push({ loc: canonicalFor(page.path), priority: page.priority });
|
|
24
|
+
}
|
|
25
|
+
for (const { collection, entries } of await allListed()) {
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
// Below every page of the site and above nothing: an entry is worth
|
|
28
|
+
// crawling and is not what the site is for.
|
|
29
|
+
urls.push({ loc: canonicalFor(href(collection, entry)), priority: 0.5 });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
35
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
36
|
+
${urls
|
|
37
|
+
.map(
|
|
38
|
+
(url) => ` <url>
|
|
39
|
+
<loc>${url.loc}</loc>
|
|
40
|
+
<priority>${url.priority.toFixed(1)}</priority>
|
|
41
|
+
</url>`,
|
|
42
|
+
)
|
|
43
|
+
.join('\n')}
|
|
44
|
+
</urlset>
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
return new Response(body, {
|
|
48
|
+
headers: { 'content-type': 'application/xml; charset=utf-8' },
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Base for {{NAME}}.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately plain. The palette, the type and the layout are decided per
|
|
5
|
+
* variant from the brief — nothing in this file is a design suggestion, and the
|
|
6
|
+
* default look must never be mistaken for a direction somebody chose.
|
|
7
|
+
*
|
|
8
|
+
* What it does establish is the token vocabulary the components expect, so that
|
|
9
|
+
* a variant fills these in rather than inventing new names. One rule matters
|
|
10
|
+
* more than the rest and is easy to get wrong:
|
|
11
|
+
*
|
|
12
|
+
* --accent the FILL. Buttons, bars, badges — anything ink sits ON.
|
|
13
|
+
* --accent-ink the same signal as TEXT. On a light ground it is a step
|
|
14
|
+
* DARKER; on a dark ground it inverts. A component that puts
|
|
15
|
+
* words on --accent is a contrast bug, not a preference.
|
|
16
|
+
*
|
|
17
|
+
* Surfaces run in one direction: --paper is the MIDDLE tone, --panel is LIFTED
|
|
18
|
+
* above it, --paper-deep is RECESSED below it. "Deep" means recessed, not dark.
|
|
19
|
+
*
|
|
20
|
+
* The preview shell drives --accent live from the studio bar, so a variant's
|
|
21
|
+
* brand colour must route through this property and everything derived from it
|
|
22
|
+
* should be derived in CSS rather than hard-coded — otherwise the picker moves
|
|
23
|
+
* one colour and leaves five behind.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Edges, entrances, and the host class a block-scope ground needs. Kept in its
|
|
28
|
+
* own file because it is a catalogue rather than a base: see
|
|
29
|
+
* `docs/surface.md` of @jtakeit/astro.
|
|
30
|
+
*/
|
|
31
|
+
@import './surface.css';
|
|
32
|
+
|
|
33
|
+
:root {
|
|
34
|
+
--paper: #ffffff;
|
|
35
|
+
--panel: #ffffff;
|
|
36
|
+
--paper-deep: #f4f2ee;
|
|
37
|
+
|
|
38
|
+
--ink: #16130f;
|
|
39
|
+
--ink-quiet: #6f6a63;
|
|
40
|
+
--line: #e2ded7;
|
|
41
|
+
|
|
42
|
+
--accent: #2f5bff;
|
|
43
|
+
--accent-ink: #1c3fd6;
|
|
44
|
+
--danger: #b3261e;
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* How wide things are allowed to be — two different questions with two
|
|
48
|
+
* different answers, and collapsing them is why a page built at 1440 looks
|
|
49
|
+
* stranded on a 27-inch monitor.
|
|
50
|
+
*
|
|
51
|
+
* --shell the layout's outer width. It GROWS with the screen.
|
|
52
|
+
* --measure how long a line of text may get. It does NOT: past about 65
|
|
53
|
+
* characters the eye loses the start of the next line.
|
|
54
|
+
*
|
|
55
|
+
* A shell capped at 1200px is 47% of a 2560px screen, and everything inside
|
|
56
|
+
* it reads as an island. Cap the measure instead and let the composition use
|
|
57
|
+
* the space. See the Composition floor in the astro-kit skill.
|
|
58
|
+
*/
|
|
59
|
+
--shell: min(92rem, 92vw);
|
|
60
|
+
--measure: 34rem;
|
|
61
|
+
|
|
62
|
+
--r-1: 4px;
|
|
63
|
+
--r-2: 8px;
|
|
64
|
+
--r-3: 14px;
|
|
65
|
+
--r-4: 22px;
|
|
66
|
+
|
|
67
|
+
--dur: 180ms;
|
|
68
|
+
--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* The surface: the ground a page is printed on, the edge a block is drawn
|
|
72
|
+
* with, and how a block arrives. Three catalogues, one reference —
|
|
73
|
+
* `docs/surface.md` of @jtakeit/astro — and the same discipline as
|
|
74
|
+
* the rest of this file: a component reads these names rather than typing a
|
|
75
|
+
* value of its own.
|
|
76
|
+
*
|
|
77
|
+
* --pattern-ink is the only colour here on purpose. Everything the ground
|
|
78
|
+
* draws is that one token at that one opacity, which is what keeps a
|
|
79
|
+
* background inside the variant's palette instead of beside it. <Pattern>
|
|
80
|
+
* sets its own geometry per ground; these are the parts a variant tunes.
|
|
81
|
+
*/
|
|
82
|
+
--pattern-ink: var(--ink);
|
|
83
|
+
|
|
84
|
+
--edge-ink: var(--line);
|
|
85
|
+
--edge-weight: 1px;
|
|
86
|
+
/* How much of a corner `corners` draws, and how deep `notch` cuts. */
|
|
87
|
+
--edge-corner: 18px;
|
|
88
|
+
--edge-offset: 7px;
|
|
89
|
+
|
|
90
|
+
--reveal-dur: 0.7s;
|
|
91
|
+
--reveal-dist: 24px;
|
|
92
|
+
/* Between one staggered child and the next. Past ~120ms a list reads as slow. */
|
|
93
|
+
--reveal-step: 80ms;
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* What a photograph is printed in. Six treatments in <Shot>, all built from
|
|
97
|
+
* these four and nothing else — see `docs/photos.md` of @jtakeit/astro.
|
|
98
|
+
*
|
|
99
|
+
* --photo-shadow carries the ground's hue on purpose. A neutral black in the
|
|
100
|
+
* shadows of a photograph on a warm page turns them grey, which is the same
|
|
101
|
+
* fault as a neutral shadow under a card and reads the same way: dirty rather
|
|
102
|
+
* than photographed.
|
|
103
|
+
*/
|
|
104
|
+
--photo-shadow: color-mix(in srgb, var(--ink) 78%, var(--accent));
|
|
105
|
+
--photo-light: var(--paper);
|
|
106
|
+
--photo-ink: var(--accent);
|
|
107
|
+
/* What `recede` veils a picture with, so copy can sit on it. */
|
|
108
|
+
--photo-veil: var(--paper);
|
|
109
|
+
--photo-amount: 1;
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* The hand.
|
|
113
|
+
*
|
|
114
|
+
* How far off square something laid down by a person sits. Measured off the
|
|
115
|
+
* sites that do this well, it is ±3–6°: past about 8° it stops reading as a
|
|
116
|
+
* hand and starts reading as a template, and the difference between a page
|
|
117
|
+
* that looks made and one that looks generated is that single number.
|
|
118
|
+
*
|
|
119
|
+
* It is a token because it has to be the *same* number everywhere on a page.
|
|
120
|
+
* Two elements at two angles nobody chose is the thing that makes a
|
|
121
|
+
* composition look accidental rather than casual.
|
|
122
|
+
*/
|
|
123
|
+
--tilt: 4deg;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
*,
|
|
127
|
+
*::before,
|
|
128
|
+
*::after {
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* An in-page link is an instant jump unless told otherwise, and on a long
|
|
134
|
+
* landing page a jump reads as a page reload: the visitor loses the thread and
|
|
135
|
+
* often scrolls back up to check what happened. Smooth scrolling is the
|
|
136
|
+
* expected behaviour here, and it is switched off wholesale for anyone who
|
|
137
|
+
* asked for less motion.
|
|
138
|
+
*/
|
|
139
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
140
|
+
html {
|
|
141
|
+
scroll-behavior: smooth;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Where a link lands. Without this the target's border box goes to the top of
|
|
147
|
+
* the screen — which on a section with generous top padding means arriving at
|
|
148
|
+
* an empty screen, with the heading the visitor came for still below the fold.
|
|
149
|
+
* Every anchor target sets its own offset: a positive one to keep air above a
|
|
150
|
+
* panel's visible edge, a negative one to pull past padding that carries no
|
|
151
|
+
* meaning. See the Composition floor in the astro-kit skill.
|
|
152
|
+
*/
|
|
153
|
+
[id] {
|
|
154
|
+
scroll-margin-block-start: 1.5rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
body {
|
|
158
|
+
margin: 0;
|
|
159
|
+
background: var(--paper);
|
|
160
|
+
color: var(--ink);
|
|
161
|
+
font-family: system-ui, sans-serif;
|
|
162
|
+
-webkit-font-smoothing: antialiased;
|
|
163
|
+
/* Nothing on the page scrolls sideways. Wide things scroll inside their own
|
|
164
|
+
container; the body never does. */
|
|
165
|
+
overflow-x: hidden;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
img,
|
|
169
|
+
picture,
|
|
170
|
+
svg,
|
|
171
|
+
video {
|
|
172
|
+
max-inline-size: 100%;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
a {
|
|
176
|
+
color: var(--accent-ink);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
button,
|
|
180
|
+
input,
|
|
181
|
+
textarea,
|
|
182
|
+
select {
|
|
183
|
+
font: inherit;
|
|
184
|
+
color: inherit;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* Autofill. Chrome and Safari repaint a filled field with their own background
|
|
189
|
+
* and their own text colour, which on any palette that is not white turns a
|
|
190
|
+
* designed form into a white-and-yellow one the moment the browser helps. There
|
|
191
|
+
* is no property to switch it off: the background has to be covered by an inset
|
|
192
|
+
* shadow, and the text colour set through -webkit-text-fill-color, which wins
|
|
193
|
+
* over `color`. The long transition is the standard trick for the repaint that
|
|
194
|
+
* fires on submit.
|
|
195
|
+
*
|
|
196
|
+
* These are the tokens the field itself uses. A variant that gives its inputs a
|
|
197
|
+
* different surface overrides this rule with the same two properties rather than
|
|
198
|
+
* deleting it.
|
|
199
|
+
*/
|
|
200
|
+
input:-webkit-autofill,
|
|
201
|
+
input:-webkit-autofill:hover,
|
|
202
|
+
input:-webkit-autofill:focus,
|
|
203
|
+
textarea:-webkit-autofill,
|
|
204
|
+
select:-webkit-autofill {
|
|
205
|
+
-webkit-text-fill-color: var(--ink);
|
|
206
|
+
-webkit-box-shadow: 0 0 0 100vmax var(--paper-deep) inset;
|
|
207
|
+
box-shadow: 0 0 0 100vmax var(--paper-deep) inset;
|
|
208
|
+
caret-color: var(--ink);
|
|
209
|
+
transition: background-color 100000s ease-in-out 0s;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Visible keyboard focus is a floor, not a feature. */
|
|
213
|
+
:focus-visible {
|
|
214
|
+
outline: 2px solid var(--accent-ink);
|
|
215
|
+
outline-offset: 2px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* Type scales with the screen and then stops. A bare vw is unbounded and a
|
|
220
|
+
* fixed rem never grows — clamp() is the whole answer, and a heading that is
|
|
221
|
+
* the same size on a laptop and on a 27-inch monitor is the commonest sign
|
|
222
|
+
* that nobody opened the page wide.
|
|
223
|
+
*/
|
|
224
|
+
h1 {
|
|
225
|
+
font-size: clamp(2rem, 1.2rem + 3vw, 4.5rem);
|
|
226
|
+
line-height: 1.05;
|
|
227
|
+
text-wrap: balance;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.btn {
|
|
231
|
+
padding: 0.75rem 1.5rem;
|
|
232
|
+
color: #fff;
|
|
233
|
+
background: var(--accent);
|
|
234
|
+
border: none;
|
|
235
|
+
border-radius: var(--r-2);
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
transition: filter var(--dur) var(--ease);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.btn:hover {
|
|
241
|
+
filter: brightness(1.08);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.btn:disabled {
|
|
245
|
+
opacity: 0.6;
|
|
246
|
+
cursor: default;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/*
|
|
250
|
+
* Reduced motion means the finished state, never a broken one. The catalogue in
|
|
251
|
+
* surface.css lands each of its own entrances in the right finished place; this
|
|
252
|
+
* is the floor under everything else.
|
|
253
|
+
*/
|
|
254
|
+
@media (prefers-reduced-motion: reduce) {
|
|
255
|
+
*,
|
|
256
|
+
*::before,
|
|
257
|
+
*::after {
|
|
258
|
+
animation-duration: 0.01ms !important;
|
|
259
|
+
animation-iteration-count: 1 !important;
|
|
260
|
+
transition-duration: 0.01ms !important;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ── the page's sections ─────────────────────────────────────────────────────
|
|
265
|
+
*
|
|
266
|
+
* A starting point, not a design. `Blocks.astro` renders whatever shape the
|
|
267
|
+
* content document has, and this gives it a readable measure, some air and a
|
|
268
|
+
* heading scale so it can be looked at before anybody has styled it. The design
|
|
269
|
+
* pass replaces all of it; what it must not remove is the annotation, which
|
|
270
|
+
* lives in the markup rather than here.
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
.hero,
|
|
274
|
+
.band {
|
|
275
|
+
padding: clamp(3rem, 8vw, 7rem) 1.25rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.hero__inner,
|
|
279
|
+
.band__inner {
|
|
280
|
+
width: var(--shell);
|
|
281
|
+
max-width: 100%;
|
|
282
|
+
margin-inline: auto;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.band:nth-of-type(even) {
|
|
286
|
+
background: var(--paper-deep);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.eyebrow {
|
|
290
|
+
margin: 0 0 0.75rem;
|
|
291
|
+
font-size: 0.8125rem;
|
|
292
|
+
letter-spacing: 0.08em;
|
|
293
|
+
text-transform: uppercase;
|
|
294
|
+
color: var(--ink-quiet);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.lead {
|
|
298
|
+
max-width: var(--measure);
|
|
299
|
+
margin: 1rem 0 0;
|
|
300
|
+
font-size: clamp(1.0625rem, 2.4vw, 1.25rem);
|
|
301
|
+
line-height: 1.6;
|
|
302
|
+
color: var(--ink-quiet);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.quiet {
|
|
306
|
+
margin: 0.25rem 0 0;
|
|
307
|
+
color: var(--ink-quiet);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.prose {
|
|
311
|
+
max-width: var(--measure);
|
|
312
|
+
margin-top: 1rem;
|
|
313
|
+
line-height: 1.7;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.prose > * + * {
|
|
317
|
+
margin-top: 0.85em;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/*
|
|
321
|
+
* A picture in a body, and pictures side by side.
|
|
322
|
+
*
|
|
323
|
+
* These two class names are the whole of what a post's pictures can look like,
|
|
324
|
+
* and `figures.mjs` is what makes them out of plain markdown: one image alone
|
|
325
|
+
* in a paragraph is a figure, several in *one* paragraph are a row, and the
|
|
326
|
+
* caption is markdown's own title slot.
|
|
327
|
+
*
|
|
328
|
+
* Restyle them. Do not invent a third — the point of a small named set is that
|
|
329
|
+
* what a body can look like was decided once by the kit rather than again by
|
|
330
|
+
* every site, and the editor can only offer what exists here.
|
|
331
|
+
*/
|
|
332
|
+
.fl-figure {
|
|
333
|
+
margin: 2rem 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.fl-figure img {
|
|
337
|
+
display: block;
|
|
338
|
+
inline-size: 100%;
|
|
339
|
+
block-size: auto;
|
|
340
|
+
border-radius: var(--radius, 0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.fl-figure figcaption {
|
|
344
|
+
margin-top: 0.6rem;
|
|
345
|
+
font-size: 0.875rem;
|
|
346
|
+
line-height: 1.5;
|
|
347
|
+
color: var(--ink-quiet);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/*
|
|
351
|
+
* A row wraps rather than shrinking past legibility: three photographs on a
|
|
352
|
+
* phone are three photographs, not three stamps. `--3` and `--4` say how many
|
|
353
|
+
* were asked for; how many actually fit is the screen's to decide.
|
|
354
|
+
*/
|
|
355
|
+
.fl-row {
|
|
356
|
+
display: grid;
|
|
357
|
+
gap: clamp(0.5rem, 1.5vw, 1rem);
|
|
358
|
+
grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
|
|
359
|
+
margin: 2rem 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.fl-row .fl-figure {
|
|
363
|
+
margin: 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Two is the one count that must not wrap on a small screen: a pair is a
|
|
367
|
+
comparison, and a comparison stacked is two pictures. */
|
|
368
|
+
.fl-row--2 {
|
|
369
|
+
grid-template-columns: repeat(2, 1fr);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.actions {
|
|
373
|
+
margin: 1.75rem 0 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.items {
|
|
377
|
+
display: grid;
|
|
378
|
+
gap: 1.5rem clamp(1.5rem, 3vw, 2.5rem);
|
|
379
|
+
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
|
|
380
|
+
margin: 2rem 0 0;
|
|
381
|
+
padding: 0;
|
|
382
|
+
list-style: none;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* An edge needs somewhere to sit: a card with no padding gets a line hard
|
|
386
|
+
against its text. The design decides the real value; this keeps the starting
|
|
387
|
+
point honest. */
|
|
388
|
+
.item[data-edge] {
|
|
389
|
+
padding: 1.25rem;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.item h3 {
|
|
393
|
+
margin: 0;
|
|
394
|
+
font-size: 1.0625rem;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.item p {
|
|
398
|
+
margin: 0.5rem 0 0;
|
|
399
|
+
line-height: 1.65;
|
|
400
|
+
color: var(--ink-quiet);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* The hero sits on the field, so its own padding has to survive the canvas
|
|
404
|
+
* behind it — the component fills its box and puts the content above. */
|
|
405
|
+
.field.hero {
|
|
406
|
+
padding: 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.field.hero .field__content {
|
|
410
|
+
padding: clamp(3rem, 8vw, 7rem) 1.25rem;
|
|
411
|
+
}
|