diff --git a/scripts/verify-artifacts.mjs b/scripts/verify-artifacts.mjs
index 103663f..89aff1d 100644
--- a/scripts/verify-artifacts.mjs
+++ b/scripts/verify-artifacts.mjs
@@ -242,12 +242,13 @@ const assertSemanticHtml = (html, inputs, contract) => {
for (const copy of requiredVisibleCopy) {
if (!semanticText.includes(copy)) throw new Error(`Missing visible semantic evidence: ${copy}`);
}
- const mailLinks = [...appRoot.querySelectorAll('a[href^="mailto:"]')]
- .filter((node) => isVisibleSemanticNode(node, appRoot));
- if (mailLinks.length !== 2 || mailLinks.some((link) =>
+ const mailLinks = [...document.querySelectorAll('a[href^="mailto:"]')];
+ const startMailLinks = [...document.querySelectorAll('#start a[href^="mailto:"]')];
+ const contactMailLinks = [...document.querySelectorAll('#contact a[href^="mailto:"]')];
+ if (mailLinks.length !== 2 || startMailLinks.length !== 1 || contactMailLinks.length !== 1 || mailLinks.some((link) =>
link.getAttribute('href') !== `mailto:${inputs.content.contact.email}` || link.textContent !== 'Email Luca'
)) {
- throw new Error('Visible semantic mailto links must be exactly the hero and contact Email Luca actions.');
+ throw new Error('Mailto links must be exactly one Email Luca action beneath each hero and contact section.');
}
const moduleScript = document.querySelector(`script[type="module"][src="/${contract.entryModule}"]`);
diff --git a/src/components/ToolkitGrid.vue b/src/components/ToolkitGrid.vue
index 9f130f2..db0e6e5 100644
--- a/src/components/ToolkitGrid.vue
+++ b/src/components/ToolkitGrid.vue
@@ -13,7 +13,7 @@ defineProps<{ groups: readonly ToolkitGroup[] }>();
diff --git a/src/styles/components.css b/src/styles/components.css
index 03d92bc..067ee9f 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -21,12 +21,14 @@
max-inline-size: 40ch;
color: var(--text-muted);
font-size: var(--step-0);
+ line-height: 1.35;
}
.hero-journey__availability {
max-inline-size: 52ch;
color: var(--text-muted);
font-size: var(--step--1);
+ line-height: 1.35;
}
.hero-journey__actions {
@@ -36,6 +38,10 @@
align-items: center;
}
+.hero-journey__actions .hero-journey__action {
+ margin-block-start: 0;
+}
+
.hero-journey__action,
.project-link,
.contact-link,
@@ -584,6 +590,10 @@
font-size: var(--step--1);
}
+.toolkit-item__label--text-only {
+ grid-column: 1 / -1;
+}
+
.brand-icon {
inline-size: 1.05rem;
block-size: 1.05rem;
diff --git a/src/styles/layout.css b/src/styles/layout.css
index fb81234..ad6e9f3 100644
--- a/src/styles/layout.css
+++ b/src/styles/layout.css
@@ -79,17 +79,14 @@
display: grid;
min-block-size: calc(100svh - var(--site-navigation-block-size));
align-items: start;
- padding: clamp(2rem, 8vh, 6rem) 4vw 16vh;
+ padding: clamp(3.5rem, 12vh, 8rem) 4vw 16vh;
scroll-margin-block-start: var(--site-navigation-block-size);
}
.hero-journey__content {
- position: relative;
- z-index: 1;
display: grid;
- min-inline-size: 0;
- inline-size: min(38rem, 50vw);
- gap: 1rem;
+ inline-size: min(34rem, 39vw);
+ gap: 0.75rem;
padding: clamp(1.4rem, 2.2vw, 2.1rem);
border: 1px solid var(--line);
border-inline-start: 3px solid var(--cyan);
diff --git a/tests/browser/semantic.spec.ts b/tests/browser/semantic.spec.ts
index d5e30ec..c6a1e1d 100644
--- a/tests/browser/semantic.spec.ts
+++ b/tests/browser/semantic.spec.ts
@@ -102,6 +102,20 @@ test('exposes the complete semantic portfolio from the generated document', asyn
await expect(page.getByRole('link', { name: 'Explore work' })).toHaveAttribute('href', '#selected-builds');
await expect(page.getByRole('link', { name: 'Email Luca' })).toHaveCount(2);
await expect(page.locator('[data-featured-operation]')).toContainText('without data loss');
+ expect(await page.locator('[data-testid="toolkit-technology"]', { hasText: 'DNS' }).locator('span').evaluate(
+ (label) => getComputedStyle(label).gridColumn,
+ )).toBe('1 / -1');
+ expect(await page.locator('.hero-journey').evaluate((hero) => {
+ const content = hero.querySelector('.hero-journey__content')!;
+ const heroStyle = getComputedStyle(hero);
+ const contentStyle = getComputedStyle(content);
+ return {
+ preservesPadding: Math.abs(Number.parseFloat(heroStyle.paddingTop) - innerHeight * 0.12) < 0.01,
+ preservesWidth: Math.abs(Number.parseFloat(contentStyle.width) - Math.min(544, innerWidth * 0.39)) < 0.1,
+ position: contentStyle.position,
+ zIndex: contentStyle.zIndex,
+ };
+ })).toEqual({ preservesPadding: true, preservesWidth: true, position: 'static', zIndex: 'auto' });
expect(
await page.locator('h1, h2, h3').evaluateAll((headings) => {
const levels = headings.map((heading) =>
diff --git a/tests/integration/verify-artifacts.test.ts b/tests/integration/verify-artifacts.test.ts
index cf149f9..7d10bdc 100644
--- a/tests/integration/verify-artifacts.test.ts
+++ b/tests/integration/verify-artifacts.test.ts
@@ -131,6 +131,16 @@ describe('artifact verification', () => {
name: 'a mailto link with an address that differs from public content',
mutate: (html: string) => html.replace('href="mailto:luca.pislaru@gmail.com">Email Luca', 'href="mailto:wrong@example.test">Email Luca'),
},
+ {
+ name: 'a hidden mailto link outside the app root',
+ mutate: (html: string) => html.replace('