265 lines
11 KiB
Plaintext
265 lines
11 KiB
Plaintext
---
|
|
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">
|
|
<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>
|
|
</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>{'live' in r && r.live && <span class="readout__live" aria-hidden="true" />}{r.value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
</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-height));
|
|
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-height) + 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__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); }
|
|
.readout dd { display: flex; align-items: center; gap: var(--space-2); }
|
|
.readout__live { position: relative; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--signal); flex: none; }
|
|
/* The live phase pulses: a ring in the signal tone grows out of the dot and fades. */
|
|
.readout__live::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
background: var(--signal);
|
|
animation: readout-pulse calc(6 * var(--dur-slow)) var(--ease) infinite;
|
|
}
|
|
@keyframes readout-pulse {
|
|
from { transform: scale(1); opacity: 0.7; }
|
|
70%, to { transform: scale(3); opacity: 0; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.readout__live::after { animation: none; opacity: 0; }
|
|
}
|
|
|
|
@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>
|