diff --git a/public/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 b/public/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 new file mode 100644 index 0000000..01769d9 Binary files /dev/null and b/public/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 differ diff --git a/public/fonts/jetbrains-mono-latin-wght-normal.woff2 b/public/fonts/jetbrains-mono-latin-wght-normal.woff2 new file mode 100644 index 0000000..cd5102a Binary files /dev/null and b/public/fonts/jetbrains-mono-latin-wght-normal.woff2 differ diff --git a/src/assets/images/background.png b/src/assets/images/background.png new file mode 100644 index 0000000..5d0ea53 Binary files /dev/null and b/src/assets/images/background.png differ diff --git a/src/assets/logo-light.webp b/src/assets/logo-light.webp new file mode 100644 index 0000000..0c2ffc2 Binary files /dev/null and b/src/assets/logo-light.webp differ diff --git a/src/components/Header.astro b/src/components/Header.astro index 3799534..675f6f8 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -17,89 +17,172 @@ const contactHref = `${home}#contact`; const path = Astro.url.pathname.replace(/\/$/, '') || '/'; const isCurrent = (href: string) => href.split('#')[0] === path && !href.includes('#'); --- -
-
- +
+
+
+ + - + - {ui.nav.contact} + {ui.nav.contact} - + +
@@ -120,4 +203,30 @@ const isCurrent = (href: string) => href.split('#')[0] === path && !href.include document.addEventListener('keydown', (e) => { if (e.key === 'Escape') set(false); }); nav.addEventListener('click', (e) => { if ((e.target as HTMLElement).closest('a')) set(false); }); } + + /* Tone: watch a one-pixel line through the middle of the bar and switch to night while a + .night section crosses it. An IntersectionObserver, so nothing runs on scroll. */ + const sections = document.querySelectorAll('.night'); + if (header && sections.length && 'IntersectionObserver' in window) { + const under = new Set(); + let observer: IntersectionObserver | undefined; + const watch = () => { + observer?.disconnect(); + under.clear(); + const mid = Math.round(header.offsetHeight / 2); + observer = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (entry.isIntersecting) under.add(entry.target); else under.delete(entry.target); + } + header.dataset.tone = under.size ? 'night' : 'paper'; + }, { rootMargin: `-${mid}px 0px -${window.innerHeight - mid - 1}px 0px` }); + sections.forEach((section) => observer!.observe(section)); + }; + watch(); + let resize: number | undefined; + window.addEventListener('resize', () => { + window.clearTimeout(resize); + resize = window.setTimeout(watch, 150); + }); + } diff --git a/src/components/Wordmark.astro b/src/components/Wordmark.astro index f9d7aef..11835b3 100644 --- a/src/components/Wordmark.astro +++ b/src/components/Wordmark.astro @@ -4,13 +4,17 @@ blinking in on every navigation; the header needs that, the footer does not. */ import logoInline from '../assets/logo.webp?inline'; import logoUrl from '../assets/logo.webp?url'; +/* Light lettering for dark surfaces, derived from the supplied logo; the mark is unchanged. */ +import logoLightInline from '../assets/logo-light.webp?inline'; +import logoLightUrl from '../assets/logo-light.webp?url'; -interface Props { class?: string; inline?: boolean } -const { class: className, inline = false } = Astro.props; +interface Props { class?: string; inline?: boolean; light?: boolean } +const { class: className, inline = false, light = false } = Astro.props; +const src = light ? (inline ? logoLightInline : logoLightUrl) : (inline ? logoInline : logoUrl); --- CipherCell
- + -
@@ -53,8 +41,10 @@ const ui = t(lang); position: relative; display: flex; align-items: flex-end; - /* Fills the screen under the header, and never less than the headline needs. */ - min-height: max(28rem, calc(100svh - var(--header-height))); + /* Runs up under the floating header so the glass bar sits on the image, fills the screen, + and is never less than the headline needs. */ + margin-top: calc(-1 * var(--header-height)); + min-height: max(28rem, 100svh); overflow: hidden; } .hero__media { position: absolute; inset: 0; } @@ -64,22 +54,10 @@ const ui = t(lang); width: 100%; height: 100%; object-fit: cover; - object-position: 60% 50%; + object-position: 50% 50%; transform-origin: 50% 60%; animation: hero-settle var(--dur-hero) var(--ease) both; } - .hero__video { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - object-fit: cover; - object-position: 60% 50%; - opacity: 0; - transition: opacity var(--dur-slow) var(--ease); - } - /* Set once the video can actually play, so the still never cuts to black. */ - .hero__video[data-ready] { opacity: 1; } .hero__overlay { position: absolute; inset: 0; @@ -109,32 +87,6 @@ const ui = t(lang); @media (prefers-reduced-motion: reduce) { .hero__img, :global(.hero__img), .hero__line { animation: none; opacity: 1; transform: none; } - .hero__video { display: none; } } - diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index 4b74b70..b2b6b60 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -51,7 +51,6 @@ const en = { }, hero: { lines: ['Security for the software', 'everything else runs on.'], - imageAlt: 'An industrial plant at dusk: pipe racks, storage tanks and a lattice mast carrying 5G antennas under an overcast sky.', }, manifesto: { lines: ['Security is built in,', 'not bolted on.'], @@ -269,7 +268,6 @@ const de: typeof en = { }, hero: { lines: ['Sicherheit für die Software,', 'auf der alles andere läuft.'], - imageAlt: 'Eine Industrieanlage in der Dämmerung: Rohrbrücken, Tanks und ein Gittermast mit 5G-Antennen unter bedecktem Himmel.', }, manifesto: { lines: ['Sicherheit wird eingebaut,', 'nicht angeschraubt.'], diff --git a/src/styles/base.css b/src/styles/base.css index a770590..7661e6d 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -18,6 +18,24 @@ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; } +/* Self-hosted JetBrains Mono, variable, latin + latin-ext. Menu bar only. */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin-wght-normal.woff2') format('woff2-variations'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/jetbrains-mono-latin-ext-wght-normal.woff2') format('woff2-variations'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + /* Reset */ *, *::before, *::after { box-sizing: border-box; } * { margin: 0; } @@ -92,6 +110,37 @@ p { overflow-wrap: break-word; } font-weight: var(--weight-regular); } +/* HUD frame: a 1px border with a small plus-shaped cross centred on each outer corner. + .hud-frame draws border and crosses; .hud-corners draws only the crosses, for an element whose + edges already sit on lines (the current menu item). Both read --line, --arm and --frame-color, + so a parent can retune them. Note: --line is redefined here as a width, not the colour token. */ +.hud-frame, +.hud-corners { + --line: var(--hud-line-width); + --arm: var(--hud-arm); + --frame-color: var(--hud-line); + position: relative; +} +.hud-frame { border: var(--line) solid var(--frame-color); } +.hud-frame::after, +.hud-corners::after { + content: ''; + position: absolute; + inset: calc(-1 * (var(--arm) + var(--line))); + pointer-events: none; + --len: calc(2 * var(--arm) + var(--line)); + --g: linear-gradient(var(--frame-color), var(--frame-color)) no-repeat; + background: + var(--g) left 0 top var(--arm) / var(--len) var(--line), + var(--g) left var(--arm) top 0 / var(--line) var(--len), + var(--g) right 0 top var(--arm) / var(--len) var(--line), + var(--g) right var(--arm) top 0 / var(--line) var(--len), + var(--g) left 0 bottom var(--arm) / var(--len) var(--line), + var(--g) left var(--arm) bottom 0 / var(--line) var(--len), + var(--g) right 0 bottom var(--arm) / var(--len) var(--line), + var(--g) right var(--arm) bottom 0 / var(--line) var(--len); +} + /* Primitives */ .container { width: 100%; diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 2b2b0a1..19bc870 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -25,6 +25,8 @@ /* Type */ --font-sans: 'Inter Tight', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + /* Menu bar only: the tactical HUD labels. */ + --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; --step-display: clamp(2.75rem, 1.2rem + 6.2vw, 7rem); --step-h2: clamp(1.9rem, 1.1rem + 2.6vw, 3.25rem); @@ -33,6 +35,7 @@ --step-title: clamp(1.6rem, 1.2rem + 1.5vw, 2.5rem); --step-body: clamp(1rem, 0.96rem + 0.2vw, 1.125rem); --step-small: 0.875rem; + --step-micro: 0.75rem; --weight-regular: 400; --weight-medium: 500; @@ -40,6 +43,7 @@ --track-display: -0.03em; --track-h2: -0.02em; --track-h3: -0.01em; + --track-caps: 0.08em; --leading-display: 1.02; --leading-h2: 1.08; @@ -81,6 +85,21 @@ /* Chrome */ --header-height: 4.5rem; + /* HUD frame (menu bar): one line width and colour for every line, borders and crosses alike. */ + --hud-line-width: 1px; + --hud-arm: 7px; + --hud-line: rgb(26 26 24 / 0.3); + --hud-line-on-night: rgb(233 229 223 / 0.3); + /* Frosted glass behind the bar. One blur, kept small so it stays cheap to composite. */ + --glass: rgb(233 229 223 / 0.76); + --glass-on-night: rgb(14 16 15 / 0.56); + --glass-solid: rgb(233 229 223 / 0.96); + --glass-solid-on-night: rgb(14 16 15 / 0.94); + --glass-blur: 14px; + /* Tints mixed from the text colour, so they work on either tone. */ + --tint-hover: 6%; + --tint-active: 9%; + /* Focus */ --focus-width: 2px; --focus-offset: 3px;