Files
homepage_relaunch/src/components/verifier/SoonHero.astro
T
2026-10-09 09:28:44 +02:00

271 lines
11 KiB
Plaintext

---
import Toast from '../Toast.astro';
import { localePath, t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const s = ui.verifier.soon;
const n = s.notify;
/* The background: a relief of thin ridge lines, computed once at build time. Each line is a row
of smooth value noise, lifted most on the right where the page is quiet and nearly flat under
the text on the left. Rows are drawn back to front and filled with night, so a nearer ridge
hides the one behind it. Colour runs through the logo blues, deep at the back to cyan in front. */
const W = 1600, H = 1000, rows = 46, steps = 160;
const hash = (x: number, y: number) => {
const h = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
return h - Math.floor(h);
};
const smooth = (t: number) => t * t * (3 - 2 * t);
const value = (x: number, y: number) => {
const xi = Math.floor(x), yi = Math.floor(y), xf = smooth(x - xi), yf = smooth(y - yi);
const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1), d = hash(xi + 1, yi + 1);
return a + (b - a) * xf + (c - a) * yf + (a - b - c + d) * xf * yf;
};
const fbm = (x: number, y: number) => value(x, y) * 0.6 + value(x * 2.1 + 5, y * 2.1) * 0.28 + value(x * 4.3 + 9, y * 4.3) * 0.12;
const ridges = Array.from({ length: rows }, (_, r) => {
const depth = r / (rows - 1);
const base = H * 0.22 + depth * H * 0.78;
const pts = Array.from({ length: steps + 1 }, (_, i) => {
const x = (i / steps) * W;
const u = x / W;
const envelope = Math.exp(-((u - 0.7) ** 2) / 0.045) * (0.35 + 0.65 * Math.sin(depth * Math.PI));
const lift = Math.max(0, fbm(u * 5.5, r * 0.22) - 0.3) * 480 * envelope;
return `${x.toFixed(1)},${(base - lift).toFixed(1)}`;
});
return {
d: `M${pts.join(' L')} L${W},${H} L0,${H} Z`,
mix: Math.round(depth * 100),
alpha: (0.35 + depth * 0.5).toFixed(2),
};
});
---
<section class="shero night" aria-labelledby="shero-title">
<div class="shero__media" aria-hidden="true">
<svg class="shero__relief" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
{ridges.map((r) => (
<path d={r.d} style={`--mix: ${r.mix}%; --alpha: ${r.alpha}`} />
))}
</svg>
<div class="shero__overlay"></div>
</div>
<div class="container shero__inner">
<!-- First in the document, last on screen: the product name, set across the full width. -->
<h1 id="shero-title" class="shero__name">{s.title[0].replace(/\.$/, '')}<span class="visually-hidden">. {s.title[1]}</span></h1>
<div class="shero__text">
<p class="eyebrow">{s.eyebrow}</p>
<p class="shero__soon" aria-hidden="true">{s.title[1]}</p>
<p class="shero__lead">{s.lead}</p>
</div>
<form
class="signup hud-frame"
id="notify"
action="/notify"
method="post"
lang={lang}
aria-labelledby="notify-title"
data-notify
data-sent={n.status.sent}
data-invalid={n.status.invalid}
data-failed={n.status.failed}
>
<div class="signup__head">
<h2 id="notify-title" class="signup__title">{n.title}</h2>
<p class="signup__body">{n.body}</p>
</div>
<input type="hidden" name="lang" value={lang} />
<input type="hidden" name="list" value="cipherverifier-alpha" />
<p class="visually-hidden" aria-hidden="true">
<label for="notify-website">{ui.contact.form.honeypot}</label>
<input type="text" id="notify-website" name="website" tabindex="-1" autocomplete="off" />
</p>
<div class="signup__field">
<label class="signup__label" for="notify-email">{n.email}</label>
<input class="signup__input" id="notify-email" name="email" type="email" autocomplete="email" placeholder={n.placeholder} required />
</div>
<div class="signup__consent">
<input id="notify-consent" name="consent" type="checkbox" value="yes" required />
<label for="notify-consent">{n.consent} <a href={localePath(lang, '/privacy')}>{n.privacy}</a></label>
</div>
<button class="button signup__submit" type="submit">{n.submit}</button>
<noscript><p class="signup__status">{n.noscript}</p></noscript>
</form>
<!-- Status strip: the menu bar's HUD language, set along the foot of the hero. -->
<dl class="readout hud-frame" aria-label={s.status.label}>
<div class="readout__cell readout__cell--head">{s.status.label}</div>
{s.status.rows.map((r) => (
<div class="readout__cell">
<dt>{r.key}</dt>
<dd>{r.value}</dd>
</div>
))}
</dl>
</div>
<Toast for="notify" labels={n.toast} />
</section>
<style>
.shero {
position: relative;
/* Runs up under the floating header, as the home hero does, so the bar takes the night tone. */
margin-top: calc(-1 * var(--header-offset));
overflow: hidden;
}
.shero__media { position: absolute; inset: 0; animation: shero-settle var(--dur-hero) var(--ease) both; transform-origin: 60% 50%; }
.shero__relief { width: 100%; height: 100%; display: block; }
.shero__relief path {
fill: var(--night);
stroke: color-mix(in oklab, var(--logo-deep), var(--logo-cyan) var(--mix));
stroke-opacity: var(--alpha);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
/* Darker on the left, where the type sits. */
.shero__overlay {
position: absolute;
inset: 0;
background: linear-gradient(to right, rgb(14 16 15 / 0.7), rgb(14 16 15 / 0) 60%);
}
.shero__inner {
position: relative;
display: grid;
gap: var(--space-8);
padding-top: calc(var(--header-offset) + var(--space-7));
min-height: 100svh;
align-content: end;
}
.shero__text { display: grid; gap: var(--space-4); justify-items: start; }
.shero__soon {
font-size: var(--step-h2);
letter-spacing: var(--track-h2);
line-height: var(--leading-h2);
font-weight: var(--weight-medium);
}
.shero__lead { max-width: 26em; color: var(--ink-muted); }
/* The name: one word across the container, its lower edge cut by the section. */
.shero__name {
order: 9;
font-size: var(--step-giant);
font-weight: var(--weight-medium);
letter-spacing: var(--track-display);
line-height: 0.8;
white-space: nowrap;
margin-bottom: -0.1em;
margin-left: -0.05em;
}
/* The one load moment: the image settles, then the type arrives line by line. */
@keyframes shero-settle { from { transform: scale(1.045); } to { transform: none; } }
.shero__soon, .shero__lead, .signup, .readout, .shero__name {
opacity: 0;
animation: shero-in var(--dur-slow) var(--ease) forwards;
}
.shero__name { animation-duration: 900ms; animation-delay: 200ms; }
.shero__soon { animation-delay: 500ms; }
.shero__lead { animation-delay: 640ms; }
.signup, .readout { animation-delay: 780ms; }
@keyframes shero-in { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
.shero__media { animation: none; }
.shero__soon, .shero__lead, .signup, .readout, .shero__name { animation: none; opacity: 1; }
}
/* Sign-up: a framed instrument panel. */
.signup {
--hud-line: var(--hud-line-on-night);
display: grid;
gap: var(--space-5);
padding: var(--space-6);
background: var(--panel-on-night);
align-content: start;
justify-items: start;
}
.signup__head { display: grid; gap: var(--space-2); }
.signup__title { font-size: var(--step-h3); letter-spacing: var(--track-h3); line-height: var(--leading-h3); font-weight: var(--weight-medium); }
.signup__body { color: var(--ink-muted); }
.signup__field { display: grid; gap: var(--space-2); width: 100%; }
.signup__label { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); }
.signup__input {
width: 100%;
min-width: 0;
background: transparent;
border: 0;
border-bottom: var(--hairline) solid var(--line);
border-radius: 0;
padding: var(--space-3) 0;
transition: border-color var(--dur-fast) var(--ease);
}
.signup__input::placeholder { color: var(--ink-muted); opacity: 0.7; }
.signup__input:focus-visible { border-bottom-color: currentColor; }
.signup__consent {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-3);
align-items: start;
font-size: var(--step-small);
line-height: var(--leading-small);
color: var(--ink-muted);
}
.signup__consent input { width: 1.1em; height: 1.1em; margin: 0.15em 0 0; accent-color: var(--paper); }
.signup__status { font-size: var(--step-small); line-height: var(--leading-small); }
.signup__consent a { color: var(--ink); text-decoration: underline; text-decoration-thickness: var(--hairline); text-underline-offset: 0.2em; }
/* Readout: stacked on small screens, one strip of cells on wide ones. */
.readout {
--hud-line: var(--hud-line-on-night);
display: grid;
font-family: var(--font-mono);
font-size: var(--step-micro);
letter-spacing: var(--track-caps);
text-transform: uppercase;
line-height: var(--leading-small);
background: var(--panel-on-night);
}
.readout__cell {
display: grid;
grid-template-columns: 8rem 1fr;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
}
.readout__cell + .readout__cell { border-top: var(--hairline) solid var(--line-on-night); }
.readout__cell--head, .readout dt { color: var(--ink-muted); }
@media (max-width: 30rem) {
.signup { padding: var(--space-5); }
}
/* Wide: the line and lead on the left, sign-up on the right, the strip, then the name. */
@media (min-width: 64rem) {
.shero__inner {
grid-template-columns: repeat(12, minmax(0, 1fr));
column-gap: var(--gap);
row-gap: var(--space-6);
align-items: end;
}
.signup { padding: var(--space-5) var(--space-6); gap: var(--space-4); }
.shero__text { grid-column: 1 / span 6; }
.signup { grid-column: 9 / span 4; }
.readout { grid-column: 1 / -1; grid-template-columns: 1fr 2fr 2fr 2fr; }
.shero__name { grid-column: 1 / -1; margin-top: var(--space-4); }
.readout__cell { grid-template-columns: 1fr; gap: var(--space-1); padding: var(--space-4) var(--space-5); }
.readout__cell + .readout__cell { border-top: 0; border-left: var(--hairline) solid var(--line-on-night); }
/* The strip's title sits in the middle of its cell, apart from the label/value cells beside it. */
.readout__cell--head { place-content: center; text-align: center; }
}
</style>
<script>
// Sends in place and answers in the toast, like the contact form; see scripts/send-form.ts.
// Without scripting the server refuses the post and redirects to /notify/error.
import { sendInPlace } from '../../scripts/send-form';
const form = document.querySelector<HTMLFormElement>('[data-notify]');
if (form) sendInPlace(form);
</script>