@morphemecms/cms 0.7.1 → 0.8.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/defaults/collections.ts +56 -0
  3. package/defaults/company.ts +13 -4
  4. package/defaults/texts.ts +17 -0
  5. package/icons/lucide/LICENSE-phosphor +21 -0
  6. package/icons/lucide/facebook-logo.svg +1 -0
  7. package/icons/lucide/linkedin-logo.svg +1 -0
  8. package/icons/lucide/pack.json +16 -1
  9. package/icons/lucide/pinterest-logo.svg +1 -0
  10. package/icons/lucide/x-logo.svg +1 -0
  11. package/icons/material-symbols/LICENSE-phosphor +21 -0
  12. package/icons/material-symbols/facebook-logo.svg +1 -0
  13. package/icons/material-symbols/linkedin-logo.svg +1 -0
  14. package/icons/material-symbols/pack.json +15 -0
  15. package/icons/material-symbols/pinterest-logo.svg +1 -0
  16. package/icons/material-symbols/x-logo.svg +1 -0
  17. package/icons/phosphor/facebook-logo.svg +1 -0
  18. package/icons/phosphor/linkedin-logo.svg +1 -0
  19. package/icons/phosphor/pinterest-logo.svg +1 -0
  20. package/icons/phosphor/x-logo.svg +1 -0
  21. package/lib/lead.d.mts +5 -0
  22. package/lib/lead.mjs +10 -0
  23. package/lib/lead.test.mjs +65 -0
  24. package/package.json +2 -1
  25. package/scripts/icon-packs.mjs +51 -13
  26. package/scripts/material-fallback.test.mjs +49 -0
  27. package/site/icons.d.mts +13 -0
  28. package/site/icons.mjs +25 -3
  29. package/site/schema.ts +2 -0
  30. package/site/site.ts +9 -2
  31. package/site/social.d.mts +37 -0
  32. package/site/social.mjs +92 -0
  33. package/site/social.test.mjs +80 -0
  34. package/site/theme.d.mts +2 -0
  35. package/site/theme.mjs +24 -10
  36. package/site/themeImport.test.mjs +40 -0
  37. package/src/EntryForm.tsx +21 -58
  38. package/src/FormFrame.tsx +5 -5
  39. package/src/Parts.tsx +8 -30
  40. package/src/Screen.tsx +12 -5
  41. package/src/editor.css +70 -5
  42. package/src/i18n/da-DK/parts.ts +1 -1
  43. package/src/i18n/da-DK/theme.ts +4 -0
  44. package/src/i18n/en-US/parts.ts +1 -1
  45. package/src/i18n/en-US/theme.ts +4 -0
  46. package/src/icon.tsx +1 -1
  47. package/src/pagePreview.tsx +21 -1
  48. package/src/postLead.d.mts +7 -0
  49. package/src/postLead.mjs +9 -0
  50. package/src/postLead.test.mjs +28 -0
  51. package/src/ui.tsx +41 -96
  52. package/starter/package.json +1 -1
  53. package/starter/src/data/social.json +14 -0
  54. package/theme/components/PostAuthor.astro +20 -0
  55. package/theme/components/PostCard.tsx +26 -0
  56. package/theme/components/PostHeader.astro +36 -0
  57. package/theme/components/PostMeta.tsx +27 -2
  58. package/theme/components/RelatedPosts.astro +29 -0
  59. package/theme/components/ShareRow.astro +48 -0
  60. package/theme/layouts/PostLayout.astro +28 -54
  61. package/theme/styles/base.css +164 -127
  62. package/theme/styles/blocks.css +160 -155
  63. package/theme/styles/components.css +43 -34
  64. package/theme/styles/global.css +316 -303
  65. package/theme/styles/ui.css +43 -34
  66. package/theme/ui/copyLink.script.ts +17 -0
@@ -1,9 +1,19 @@
1
- // Date and reading time for a blog post: "12. marts 2024 · 5 min. læsetid" in Danish (posts.ts makes them).
1
+ // Date and reading time for a blog post, then its tags as links: "12. marts 2024 · 5 min. læsetid · News, Tips" in Danish
2
+ // (posts.ts makes them). Props: post, optional tags and className. A theme may replace this file.
3
+ import { Fragment } from 'react';
2
4
  import { Svg } from '@morphemecms/cms/src/icon';
3
5
  import type { PostView } from '@theme/components/posts';
4
6
  import { siteT } from '@morphemecms/cms/src/i18n';
5
7
 
6
- export default function PostMeta({ post, className }: { post: PostView; className?: string }) {
8
+ export default function PostMeta({
9
+ post,
10
+ tags,
11
+ className,
12
+ }: {
13
+ post: PostView;
14
+ tags?: { label: string; href: string }[];
15
+ className?: string;
16
+ }) {
7
17
  return (
8
18
  <p className={['flex flex-wrap items-center gap-x-2 type-body-medium text-muted', className].filter(Boolean).join(' ')}>
9
19
  <time dateTime={post.date}>{post.dateText}</time>
@@ -12,6 +22,21 @@ export default function PostMeta({ post, className }: { post: PostView; classNam
12
22
  <Svg name="clock" size={15} />
13
23
  {siteT.theme.readingTime(post.minutes)}
14
24
  </span>
25
+ {tags && tags.length > 0 && (
26
+ <>
27
+ <span aria-hidden="true">·</span>
28
+ <span>
29
+ {tags.map((tag, i) => (
30
+ <Fragment key={tag.href}>
31
+ {i > 0 && ', '}
32
+ <a href={tag.href} className="underline hover:text-accent">
33
+ {tag.label}
34
+ </a>
35
+ </Fragment>
36
+ ))}
37
+ </span>
38
+ </>
39
+ )}
15
40
  </p>
16
41
  );
17
42
  }
@@ -0,0 +1,29 @@
1
+ ---
2
+ // A "Related posts" section under a blog post: up to three PostCard components in a responsive grid. A theme may replace
3
+ // this file to redesign the section.
4
+ import Container from '@theme/components/Container.astro';
5
+ import PostCard from '@theme/components/PostCard';
6
+ import { siteT } from '@morphemecms/cms/src/i18n';
7
+ import type { PostView } from '@theme/components/posts';
8
+
9
+ interface Props {
10
+ posts: PostView[];
11
+ }
12
+
13
+ const { posts } = Astro.props;
14
+ ---
15
+
16
+ {posts.length > 0 && (
17
+ <Container class="mt-12 medium:mt-16" aria-labelledby="related-posts">
18
+ <h2 id="related-posts" class="type-headline-small">
19
+ {siteT.theme.relatedPosts}
20
+ </h2>
21
+ <ul class="mt-6 grid gap-6 medium:grid-cols-3">
22
+ {posts.map((post) => (
23
+ <li>
24
+ <PostCard post={post} />
25
+ </li>
26
+ ))}
27
+ </ul>
28
+ </Container>
29
+ )}
@@ -0,0 +1,48 @@
1
+ ---
2
+ // The share buttons under a blog post: plain links to Facebook, X, LinkedIn and Pinterest, and a button that copies the
3
+ // post's address (theme/ui/copyLink.script.ts). Props: the post's address, title and picture, and which buttons are on
4
+ // (site.social.share). Draws nothing without buttons. Uses shareLinks (site/social.mjs), Icon and IconButton's classes.
5
+ import { shareLinks, type ShareId } from '@morphemecms/cms/site/social.mjs';
6
+ import Icon from '@theme/components/Icon.astro';
7
+ import { iconButtonClass } from '@theme/ui/IconButton';
8
+ import { siteT } from '@morphemecms/cms/src/i18n';
9
+
10
+ interface Props {
11
+ url: string;
12
+ title: string;
13
+ picture?: string;
14
+ share: ShareId[];
15
+ }
16
+ const { url, title, picture, share } = Astro.props;
17
+
18
+ const links = shareLinks({ url, title, picture }, share);
19
+ const networks: Record<string, { name: string; icon: string }> = {
20
+ facebook: { name: 'Facebook', icon: 'facebook-logo' },
21
+ x: { name: 'X', icon: 'x-logo' },
22
+ linkedin: { name: 'LinkedIn', icon: 'linkedin-logo' },
23
+ pinterest: { name: 'Pinterest', icon: 'pinterest-logo' },
24
+ };
25
+ const buttonClass = iconButtonClass('standard');
26
+ ---
27
+
28
+ {links.length > 0 && (
29
+ <div role="group" aria-label={siteT.theme.share} class="flex items-center gap-x-2">
30
+ {links.map(({ id, href }) =>
31
+ id === 'copy-link' ? (
32
+ <button
33
+ type="button"
34
+ class={buttonClass}
35
+ aria-label={siteT.theme.copyLink}
36
+ data-copy-link={url}
37
+ data-copied={siteT.theme.linkCopied}
38
+ >
39
+ <Icon name="link-simple" size={22} />
40
+ </button>
41
+ ) : (
42
+ <a href={href} class={buttonClass} aria-label={siteT.theme.shareOn(networks[id].name)} target="_blank" rel="noopener">
43
+ <Icon name={networks[id].icon} size={22} />
44
+ </a>
45
+ ),
46
+ )}
47
+ </div>
48
+ )}
@@ -1,16 +1,18 @@
1
1
  ---
2
- // Blog post template. A narrow reading column, the site's owner as author; related posts in the footer.
2
+ // Blog post template. A narrow reading column, the site's owner as author. Order: PostHeader (back link, title, author,
3
+ // byline with tags, share row), the lead (the text's first paragraph), the featured picture, the rest of the text, the
4
+ // end block, RelatedPosts. Without a lead the picture comes before the text.
3
5
  // data-editor marks what the editor's preview fills in with what is in its form ("Forhåndsvis",
4
- // packages/editor/src/pagePreview.tsx): the heading, the picture and the text. The text goes through RichText (blocks={false}: a post draws no blocks, but its coloured sections are panels).
6
+ // packages/editor/src/pagePreview.tsx): the heading, the lead, the picture and the text. The text goes through RichText (blocks={false}: a post draws no blocks, but its coloured sections are panels).
7
+ import { splitLead } from '@morphemecms/cms/lib/lead.mjs';
5
8
  import { firstImage, heroImage, picturesOf } from '@morphemecms/cms/site/images.mjs';
6
9
  import Base from '@theme/layouts/Base.astro';
7
- import Icon from '@theme/components/Icon.astro';
8
10
  import Img from '@theme/components/Img.astro';
9
11
  import EndBlock from '@theme/components/EndBlock.astro';
10
12
  import RichText from '@theme/components/RichText.astro';
11
13
  import { categoryName, getPosts, postPath, stateOf, type Post } from '@morphemecms/cms/site/content';
12
- import PostMeta from '@theme/components/PostMeta';
13
- import Chip from '@theme/ui/Chip';
14
+ import PostHeader from '@theme/components/PostHeader.astro';
15
+ import RelatedPosts from '@theme/components/RelatedPosts.astro';
14
16
  import { postViewOf } from '@theme/components/loadPosts';
15
17
  import { site } from '@morphemecms/cms/site/site';
16
18
  import { isBlogCategory } from '@morphemecms/cms/site/categories';
@@ -24,11 +26,20 @@ const { post } = Astro.props;
24
26
  const { title, description, date, updated, categories } = post.data;
25
27
  // A decorative picture (imageDecorative) has no alternative text.
26
28
  const imageAlt = post.data.imageDecorative ? undefined : post.data.imageAlt;
27
- // The pills under the post: only the blog categories (the others have no page).
29
+ // The tags in the byline: only the blog categories (the others have no page).
28
30
  const onBlog = categories.filter(isBlogCategory);
29
31
  // Some posts also have their featured image (or another version of it) inside the text; never show it twice.
30
32
  const image = heroImage(post.data.image, post.body);
31
33
 
34
+ // The picture a share to Pinterest pins: the one chosen for sharing, else the post's own, with the site's address in front.
35
+ const sharePicture = post.data.shareImage || post.data.image || firstImage(post.body);
36
+ const picture = sharePicture?.startsWith('/') ? site.url + sharePicture : sharePicture || undefined;
37
+
38
+ // The text's first paragraph is the lead, drawn before the featured picture; without one the picture comes first.
39
+ const { lead, rest } = splitLead(await Astro.slots.render('default'));
40
+ // The lead has the text's class too (the editor's preview copies the text's class to the lead) and post-lead.
41
+ const textClass = 'prose mx-auto mt-10 max-w-3xl px-5 medium:mt-14 medium:px-8';
42
+
32
43
  // Related: most shared categories first, then newest.
33
44
  const related = (await getPosts())
34
45
  .filter((p) => p.id !== post.id)
@@ -69,23 +80,14 @@ const related = (await getPosts())
69
80
  ]}
70
81
  >
71
82
  <article>
72
- <header class="mx-auto max-w-3xl px-5 pt-8 medium:px-8 medium:pt-14">
73
- <a href="/blog/" class="inline-flex items-center gap-1.5 type-body-medium text-muted hover:text-accent">
74
- <Icon name="arrow-left" size={16} /> {siteT.theme.blog}
75
- </a>
76
- <h1 data-editor="heading" class="theme-title mt-3">
77
- {title}
78
- </h1>
79
- <div class="mt-6 flex items-center gap-3">
80
- {site.portrait && (
81
- <Img src={site.portrait} alt="" loading="eager" class="size-11 rounded-full object-cover object-[50%_20%]" sizes="44px" />
82
- )}
83
- <div class="leading-snug">
84
- <span class="block font-medium">{site.name}</span>
85
- <PostMeta post={postViewOf(post)} />
86
- </div>
87
- </div>
88
- </header>
83
+ <PostHeader
84
+ title={title}
85
+ post={postViewOf(post)}
86
+ tags={onBlog.map((c) => ({ label: categoryName(c), href: `/blog/kategori/${c}/` }))}
87
+ share={{ url: site.url + postPath(post), title, picture, share: site.social.share }}
88
+ />
89
+
90
+ {lead && <div data-editor="lead" class={`${textClass} post-lead`} set:html={lead} />}
89
91
 
90
92
  {image && (
91
93
  <div data-editor="image" class="mx-auto mt-8 max-w-5xl px-5 medium:mt-12 medium:px-8">
@@ -99,40 +101,12 @@ const related = (await getPosts())
99
101
  </div>
100
102
  )}
101
103
 
102
- <div data-editor="text" class="prose mx-auto mt-10 max-w-3xl px-5 medium:mt-14 medium:px-8">
103
- <RichText html={await Astro.slots.render('default')} blocks={false} />
104
+ <div data-editor="text" class={textClass}>
105
+ <RichText html={rest} blocks={false} />
104
106
  </div>
105
-
106
- <footer class="mx-auto mt-12 max-w-3xl px-5 medium:px-8">
107
- {onBlog.length > 0 && (
108
- <ul class="flex flex-wrap gap-2" aria-label="Tags">
109
- {onBlog.map((c) => (
110
- <li>
111
- <Chip href={`/blog/kategori/${c}/`} label={categoryName(c)} />
112
- </li>
113
- ))}
114
- </ul>
115
- )}
116
- </footer>
117
107
  </article>
118
108
 
119
109
  {site.postEnd && <EndBlock type={site.postEnd} />}
120
110
 
121
- {related.length > 0 && (
122
- <Fragment slot="footer-column">
123
- <p class="footer-heading font-medium">{siteT.theme.relatedPosts}</p>
124
- <ul class="mt-3 space-y-2.5 text-muted">
125
- {related.map((p) => (
126
- <li>
127
- <a href={postPath(p)} class="hover:text-accent">
128
- {p.data.title}
129
- </a>
130
- </li>
131
- ))}
132
- </ul>
133
- <a href="/blog/" class="mt-4 inline-flex items-center gap-1.5 font-medium text-accent">
134
- {siteT.theme.seeAll} <Icon name="arrow-right" size={16} />
135
- </a>
136
- </Fragment>
137
- )}
111
+ <RelatedPosts posts={related.map(postViewOf)} />
138
112
  </Base>
@@ -1,5 +1,7 @@
1
1
  @import 'tailwindcss';
2
2
  @plugin '@tailwindcss/typography';
3
+ /* Material's rules sit in the last layer, fallback: a theme's own unlayered rules beat them, whatever their specificity. */
4
+ @layer theme, base, components, utilities, fallback;
3
5
  /* The theme's UI components, as Material 3 names them (buttons, icon buttons), and the standard blocks of the editor,
4
6
  in this site's design (their variables below). */
5
7
  @import './ui.css';
@@ -8,6 +10,9 @@
8
10
  @import './components.css';
9
11
 
10
12
  /*
13
+ Material is every theme's fallback: its rules that set more than custom properties sit in @layer fallback (declared last in
14
+ base.css), so a theme's own unlayered rules win over them, whatever the specificity. Custom properties stay unlayered.
15
+
11
16
  The standard theme's base: what every theme builds on, plain and neutral. The system's own font, a small radius on
12
17
  panels, a light shadow, full pills on controls; the classes the standard theme's components use (.link, .prose,
13
18
  the motion …) besides the UI components (ui.css: buttons, icon buttons, chips, snackbars). The standard theme's own look is over it, in global.css; a theme with a look of its
@@ -31,7 +36,6 @@
31
36
  --shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 8px 24px -12px rgb(0 0 0 / 0.18);
32
37
  /* The motion scheme's curve (blocks.css, lib/motion.ts). */
33
38
  --ease: var(--motion-ease, cubic-bezier(0.2, 0, 0, 1));
34
- color-scheme: light;
35
39
 
36
40
  /* The standard blocks (blocks.css) in this design; in dark mode too, as they
37
41
  follow the tokens above. */
@@ -53,17 +57,34 @@
53
57
  /* The same for a chip set that scrolls (ui.css). */
54
58
  --chip-set-bleed: 1.25rem;
55
59
  }
60
+ @layer fallback {
61
+ :root {
62
+ color-scheme: light;
63
+ }
64
+ }
56
65
 
57
66
  @media (prefers-color-scheme: dark) {
58
67
  :root:not([data-theme='light']) {
59
68
  --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 20px 40px -24px rgb(0 0 0 / 0.7);
60
- color-scheme: dark;
69
+ }
70
+ }
71
+
72
+ @layer fallback {
73
+ @media (prefers-color-scheme: dark) {
74
+ :root:not([data-theme='light']) {
75
+ color-scheme: dark;
76
+ }
61
77
  }
62
78
  }
63
79
 
64
80
  :root[data-theme='dark'] {
65
81
  --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 20px 40px -24px rgb(0 0 0 / 0.7);
66
- color-scheme: dark;
82
+ }
83
+
84
+ @layer fallback {
85
+ :root[data-theme='dark'] {
86
+ color-scheme: dark;
87
+ }
67
88
  }
68
89
 
69
90
  @theme inline {
@@ -176,152 +197,168 @@
176
197
  --tw-prose-th-borders: var(--line);
177
198
  --tw-prose-td-borders: var(--line);
178
199
  --tw-prose-code: var(--ink);
179
- font-size: var(--type-body-large-size);
180
- line-height: var(--type-body-large-line-height);
181
- font-weight: var(--type-body-large-weight);
182
- letter-spacing: var(--type-body-large-tracking);
183
- }
184
- /* The text's headings: a headline, a title under it and a smaller title under that (the type scale, ui.css). */
185
- .prose :where(h2) {
186
- font-size: var(--type-headline-medium-size);
187
- line-height: var(--type-headline-medium-line-height);
188
- font-weight: var(--type-headline-medium-weight);
189
- letter-spacing: var(--type-headline-medium-tracking);
190
- }
191
- .prose :where(h3) {
192
- font-size: var(--type-title-large-size);
193
- line-height: var(--type-title-large-line-height);
194
- font-weight: var(--type-title-large-weight);
195
- letter-spacing: var(--type-title-large-tracking);
196
- }
197
- .prose :where(h4) {
198
- font-size: var(--type-title-medium-size);
199
- line-height: var(--type-title-medium-line-height);
200
- font-weight: var(--type-title-medium-weight);
201
- letter-spacing: var(--type-title-medium-tracking);
202
- }
203
- /* Code in a line (cookie names in the privacy policy): on a tint, without the plugin's backticks. */
204
- .prose :where(:not(pre) > code):not(:where(.not-prose code)) {
205
- background: var(--tint);
206
- border-radius: 0.3em;
207
- padding: 0.1em 0.35em;
208
- font-weight: 500;
209
200
  }
210
- .prose :where(:not(pre) > code):not(:where(.not-prose code))::before,
211
- .prose :where(:not(pre) > code):not(:where(.not-prose code))::after {
212
- content: none;
213
- }
214
- /* Not the links of a block in the text (not-prose): their look is the block's. */
215
- .prose :where(a):not(:where(.not-prose a)) {
216
- font-weight: 500;
217
- text-decoration-thickness: 1px;
218
- text-underline-offset: 0.2em;
219
- }
220
- /* Pictures in the text; not those in a block of the site in it (not-prose, RichText.astro). */
221
- .prose :where(img):not(:where(.not-prose img)) {
222
- border-radius: var(--radius);
223
- width: 100%;
224
- height: auto;
225
- }
226
- /* A picture in the text cut to a format in the editor (packages/editor/lib/crop.mjs): the box is the format, the
201
+ @layer fallback {
202
+ .prose {
203
+ font-size: var(--type-body-large-size);
204
+ line-height: var(--type-body-large-line-height);
205
+ font-weight: var(--type-body-large-weight);
206
+ letter-spacing: var(--type-body-large-tracking);
207
+ }
208
+ /* A blog post's lead: its first paragraph, one step larger than the text (the title-large size, with the body weight). */
209
+ .prose.post-lead {
210
+ font-size: var(--type-title-large-size);
211
+ line-height: var(--type-title-large-line-height);
212
+ font-weight: var(--type-body-large-weight);
213
+ letter-spacing: var(--type-title-large-tracking);
214
+ }
215
+ /* The text right after the lead (no picture between): the space the prose puts between paragraphs (1.25em), not the
216
+ larger space of a text under a picture. Unlayered on purpose: it must beat the mt-10 / medium:mt-14 utilities (a layer). */
217
+ .prose.post-lead + .prose {
218
+ margin-top: 1.25em;
219
+ }
220
+ /* The text's headings: a headline, a title under it and a smaller title under that (the type scale, ui.css). */
221
+ .prose :where(h2) {
222
+ font-size: var(--type-headline-medium-size);
223
+ line-height: var(--type-headline-medium-line-height);
224
+ font-weight: var(--type-headline-medium-weight);
225
+ letter-spacing: var(--type-headline-medium-tracking);
226
+ }
227
+ .prose :where(h3) {
228
+ font-size: var(--type-title-large-size);
229
+ line-height: var(--type-title-large-line-height);
230
+ font-weight: var(--type-title-large-weight);
231
+ letter-spacing: var(--type-title-large-tracking);
232
+ }
233
+ .prose :where(h4) {
234
+ font-size: var(--type-title-medium-size);
235
+ line-height: var(--type-title-medium-line-height);
236
+ font-weight: var(--type-title-medium-weight);
237
+ letter-spacing: var(--type-title-medium-tracking);
238
+ }
239
+ /* Code in a line (cookie names in the privacy policy): on a tint, without the plugin's backticks. */
240
+ .prose :where(:not(pre) > code):not(:where(.not-prose code)) {
241
+ background: var(--tint);
242
+ border-radius: 0.3em;
243
+ padding: 0.1em 0.35em;
244
+ font-weight: 500;
245
+ }
246
+ .prose :where(:not(pre) > code):not(:where(.not-prose code))::before,
247
+ .prose :where(:not(pre) > code):not(:where(.not-prose code))::after {
248
+ content: none;
249
+ }
250
+ /* Not the links of a block in the text (not-prose): their look is the block's. */
251
+ .prose :where(a):not(:where(.not-prose a)) {
252
+ font-weight: 500;
253
+ text-decoration-thickness: 1px;
254
+ text-underline-offset: 0.2em;
255
+ }
256
+ /* Pictures in the text; not those in a block of the site in it (not-prose, RichText.astro). */
257
+ .prose :where(img):not(:where(.not-prose img)) {
258
+ border-radius: var(--radius);
259
+ width: 100%;
260
+ height: auto;
261
+ }
262
+ /* A picture in the text cut to a format in the editor (packages/editor/lib/crop.mjs): the box is the format, the
227
263
  picture in it as large and as far moved as the part chosen. */
228
- .picture-crop {
229
- display: block;
230
- position: relative;
231
- overflow: hidden;
232
- border-radius: var(--radius);
233
- margin-block: 2em;
234
- }
235
- .picture-crop > img {
236
- position: absolute;
237
- inset: 0 auto auto 0;
238
- max-width: none;
239
- height: auto;
240
- margin: 0;
241
- border-radius: 0;
242
- }
243
- /* Video embeds (an embed that needs another size, an app's calendar, sets its own). */
244
- .prose :where(iframe) {
245
- width: 100%;
246
- aspect-ratio: 16 / 9;
247
- height: auto;
248
- border: 0;
249
- border-radius: var(--radius);
250
- margin-block: 2em;
251
- }
264
+ .picture-crop {
265
+ display: block;
266
+ position: relative;
267
+ overflow: hidden;
268
+ border-radius: var(--radius);
269
+ margin-block: 2em;
270
+ }
271
+ .picture-crop > img {
272
+ position: absolute;
273
+ inset: 0 auto auto 0;
274
+ max-width: none;
275
+ height: auto;
276
+ margin: 0;
277
+ border-radius: 0;
278
+ }
279
+ /* Video embeds (an embed that needs another size, an app's calendar, sets its own). */
280
+ .prose :where(iframe) {
281
+ width: 100%;
282
+ aspect-ratio: 16 / 9;
283
+ height: auto;
284
+ border: 0;
285
+ border-radius: var(--radius);
286
+ margin-block: 2em;
287
+ }
252
288
 
253
- /* Motion.
289
+ /* Motion.
254
290
  [data-reveal]: sections fade up once when scrolled into view (packages/editor/theme/scripts/motion.ts).
255
291
  The top of a page stands still from the first paint; from another page of the site it fades in (components.css).
256
292
  Off for visitors who prefer reduced motion, and with the calm motion scheme ("Bevægelse", data-motion-scheme). */
257
- @media (prefers-reduced-motion: no-preference) {
258
- .js:not([data-motion-scheme='calm']) [data-reveal] {
259
- opacity: 0;
293
+ @media (prefers-reduced-motion: no-preference) {
294
+ .js:not([data-motion-scheme='calm']) [data-reveal] {
295
+ opacity: 0;
296
+ }
260
297
  }
261
- }
262
298
 
263
- /* The site's logo (public/logo.svg), colored by background-color via a mask. */
264
- .logo-mark {
265
- display: inline-block;
266
- -webkit-mask: url('/logo.svg') center / contain no-repeat;
267
- mask: url('/logo.svg') center / contain no-repeat;
268
- }
299
+ /* The site's logo (public/logo.svg), colored by background-color via a mask. */
300
+ .logo-mark {
301
+ display: inline-block;
302
+ -webkit-mask: url('/logo.svg') center / contain no-repeat;
303
+ mask: url('/logo.svg') center / contain no-repeat;
304
+ }
269
305
 
270
- /* A block the theme places outside the text (Block.astro in EndBlock.astro and on the prices page): its
306
+ /* A block the theme places outside the text (Block.astro in EndBlock.astro and on the prices page): its
271
307
  place gives the space around it, not the text's (.block-in-text). */
272
- .block-place > .block-in-text {
273
- margin-block: 0;
274
- }
275
- /* The heading of a block placed outside the text (a front-page section): as the theme's other section headings
308
+ .block-place > .block-in-text {
309
+ margin-block: 0;
310
+ }
311
+ /* The heading of a block placed outside the text (a front-page section): as the theme's other section headings
276
312
  (SectionHeading.tsx), not the text's; with a label or a subheading (.block-head, blocks.css) the space under the
277
313
  whole head. */
278
- .block-place > h2,
279
- .block-place > .block-head {
280
- margin-block: 0 2rem;
281
- }
282
- .block-place > h2,
283
- .block-place > .block-head > h2 {
284
- font-size: var(--type-headline-large-size);
285
- line-height: var(--type-headline-large-line-height);
286
- font-weight: var(--type-headline-large-weight);
287
- letter-spacing: var(--type-headline-large-tracking);
288
- }
289
- @media (width >= theme(--breakpoint-medium)) {
314
+ .block-place > h2,
315
+ .block-place > .block-head {
316
+ margin-block: 0 2rem;
317
+ }
290
318
  .block-place > h2,
291
319
  .block-place > .block-head > h2 {
292
- font-size: var(--type-display-medium-size);
293
- line-height: var(--type-display-medium-line-height);
294
- font-weight: var(--type-display-medium-weight);
295
- letter-spacing: var(--type-display-medium-tracking);
320
+ font-size: var(--type-headline-large-size);
321
+ line-height: var(--type-headline-large-line-height);
322
+ font-weight: var(--type-headline-large-weight);
323
+ letter-spacing: var(--type-headline-large-tracking);
296
324
  }
297
- }
298
- /* Cards placed in the page's full width (a front-page section, PageBlock.astro): three in a row on a computer, as
325
+ @media (width >= theme(--breakpoint-medium)) {
326
+ .block-place > h2,
327
+ .block-place > .block-head > h2 {
328
+ font-size: var(--type-display-medium-size);
329
+ line-height: var(--type-display-medium-line-height);
330
+ font-weight: var(--type-display-medium-weight);
331
+ letter-spacing: var(--type-display-medium-tracking);
332
+ }
333
+ }
334
+ /* Cards placed in the page's full width (a front-page section, PageBlock.astro): three in a row on a computer, as
299
335
  there is room for (in the text, two). */
300
- @media (width >= theme(--breakpoint-expanded)) {
301
- .block-place .block-cards {
302
- grid-template-columns: repeat(3, minmax(0, 1fr));
336
+ @media (width >= theme(--breakpoint-expanded)) {
337
+ .block-place .block-cards {
338
+ grid-template-columns: repeat(3, minmax(0, 1fr));
339
+ }
303
340
  }
304
- }
305
341
 
306
- /* The mark before the name on a testimonial (the standard block's card): a small dot in the accent, which the standard
342
+ /* The mark before the name on a testimonial (the standard block's card): a small dot in the accent, which the standard
307
343
  theme's own look (global.css) makes an avatar with the initial. Another mark (e.g. a logo) is that
308
344
  theme's own (its global.css), not the standard theme's. */
309
- .block-testimonial-mark {
310
- display: inline-block;
311
- flex-shrink: 0;
312
- width: 0.5rem;
313
- height: 0.5rem;
314
- border-radius: 9999px;
315
- background-color: var(--accent);
316
- }
345
+ .block-testimonial-mark {
346
+ display: inline-block;
347
+ flex-shrink: 0;
348
+ width: 0.5rem;
349
+ height: 0.5rem;
350
+ border-radius: 9999px;
351
+ background-color: var(--accent);
352
+ }
317
353
 
318
- /* Boxes side by side ("Links", an app's boxes): the site's panels (outlined cards, ui.css, without a shadow), with
354
+ /* Boxes side by side ("Links", an app's boxes): the site's panels (outlined cards, ui.css, without a shadow), with
319
355
  more room on a computer and the heading of a section in it. */
320
- .block-panels .block-panel-title {
321
- font-size: var(--type-headline-small-size);
322
- line-height: var(--type-headline-small-line-height);
323
- font-weight: var(--type-headline-small-weight);
324
- letter-spacing: var(--type-headline-small-tracking);
356
+ .block-panels .block-panel-title {
357
+ font-size: var(--type-headline-small-size);
358
+ line-height: var(--type-headline-small-line-height);
359
+ font-weight: var(--type-headline-small-weight);
360
+ letter-spacing: var(--type-headline-small-tracking);
361
+ }
325
362
  }
326
363
  @media (width >= theme(--breakpoint-medium)) {
327
364
  .block-panels > .block-panel {