From b2f8672952361c1e2fde64a1de779af83fcc996d Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 6 Jul 2026 22:53:03 -0500 Subject: [PATCH 1/3] =?UTF-8?q?Redesign=20hero=20as=20'the=20summoning'=20?= =?UTF-8?q?=E2=80=94=20continuity-first=20hero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New thesis headline 'Summon agents that remember.' with Fraunces italic as the single display-type moment (new dep: @fontsource-variable/fraunces, italic axis only) - Download CTA collapsed from 4-chip grid + heading + badge into one platform-detected primary button and a quiet 'also on' mono row; JS retargets the primary and keeps all four platforms one click away (works fully without JS) - Terminal card restructured as a real session: typed command on top, familiar state (sigil, role, memory restored) as output, roster tabs with arrow-key tablist navigation - Signature element: continuity timeline across the hero base plotting a familiar's memory over 47 days, ending in a pulsing 'today' node; replaces the icon feature grid that duplicated ProofGrid; rotates to a vertical thread on mobile - Contrast bumps on small mono labels; reduced-motion and no-JS paths verified; verify-static.mjs assertions updated to the new CTA Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package.json | 1 + pnpm-lock.yaml | 8 + scripts/verify-static.mjs | 24 +- src/components/DownloadCTA.astro | 116 ++--- src/components/Hero.astro | 164 ++++--- src/pages/index.astro | 3 + src/scripts/main.js | 152 +++--- src/styles/global.css | 808 ++++++++++++++++--------------- 8 files changed, 651 insertions(+), 625 deletions(-) diff --git a/package.json b/package.json index b92d570..6fc3948 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "check": "node scripts/verify-static.mjs" }, "dependencies": { + "@fontsource-variable/fraunces": "^5.2.9", "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", "astro": "^5.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e17aab..9ebb06e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@fontsource-variable/fraunces': + specifier: ^5.2.9 + version: 5.2.9 '@fontsource-variable/inter': specifier: ^5.2.8 version: 5.2.8 @@ -373,6 +376,9 @@ packages: cpu: [x64] os: [win32] + '@fontsource-variable/fraunces@5.2.9': + resolution: {integrity: sha512-Y6IjunlN9Ni723np+GIgAaKzCDBrPRrqNi01TZxHs5wtHYROWFM9W6yiT+/gGwSjWIRD18oX17kD/BRWekc/Lw==} + '@fontsource-variable/inter@5.2.8': resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} @@ -1913,6 +1919,8 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@fontsource-variable/fraunces@5.2.9': {} + '@fontsource-variable/inter@5.2.8': {} '@fontsource-variable/jetbrains-mono@5.2.8': {} diff --git a/scripts/verify-static.mjs b/scripts/verify-static.mjs index 11d001f..55d5cc2 100644 --- a/scripts/verify-static.mjs +++ b/scripts/verify-static.mjs @@ -74,14 +74,22 @@ if (existsSync(distIndex)) { } const downloadLabels = [ - 'Download for macOS', - 'Download for Windows', - 'Download for Linux', - 'Get the iOS Beta', + 'Download for macOS', // server-rendered primary (JS retargets per platform) + 'Windows', + 'Linux', + 'iOS (TestFlight)', + 'all releases', ]; + // Scope to the CTA block — words like "Windows" also appear in the + // head's JSON-LD, which would confuse a whole-document indexOf. + const ctaStart = html.indexOf('data-download-cta'); + if (ctaStart === -1) { + throw new Error('dist/index.html is missing the data-download-cta block'); + } + const ctaHtml = html.slice(ctaStart); const downloadLabelPositions = downloadLabels.map((label) => ({ label, - index: html.indexOf(label), + index: ctaHtml.indexOf(label), })); const missingDownloadLabels = downloadLabelPositions .filter(({ index }) => index === -1) @@ -91,13 +99,13 @@ if (existsSync(distIndex)) { } for (let i = 1; i < downloadLabelPositions.length; i += 1) { if (downloadLabelPositions[i - 1].index > downloadLabelPositions[i].index) { - throw new Error('Download CTA order must be macOS, Windows, Linux, then iOS beta'); + throw new Error('Download CTA order must be the macOS primary, then Windows, Linux, iOS beta, and all releases'); } } const css = await readFile(path.join(root, 'src/styles/global.css'), 'utf8'); - if (!css.includes('.download-btn[data-platform="ios"]') || !css.includes('grid-column: 1 / -1')) { - throw new Error('Download CTA must make the bottom iOS beta button span the full button grid width'); + if (!css.includes('.download-alt.is-detected') || !html.includes('data-download-primary')) { + throw new Error('Download CTA must render one retargetable primary button and hide the detected platform from the alt row'); } console.log( `Verified ${requiredPublicFiles.length} required public files, canonical favicon + OG logos, and ${requiredCopy.length} required copy strings in dist/index.html.`, diff --git a/src/components/DownloadCTA.astro b/src/components/DownloadCTA.astro index c605b6b..d6236c7 100644 --- a/src/components/DownloadCTA.astro +++ b/src/components/DownloadCTA.astro @@ -1,94 +1,64 @@ --- -// Predominant "Download" call-to-action for the Hero. +// Platform-aware download CTA — one decision, not four. // -// Renders four platform buttons (macOS · iOS/iPadOS · Windows · Linux) plus -// an "all releases & checksums" utility link. Client-side script detects the -// visitor's platform (UA + UA-CH + navigator.platform, with an iPad-on-iOS-13+ -// disambiguation) and: -// 1. Promotes the matching button to the visually-primary slot. -// 2. Updates the platform name in the headline (e.g. "CovenCave for macOS"). -// Server-rendered markup is fully functional without JS — every button is -// a real link, every platform link points at the canonical release/asset -// URL or TestFlight. The script only changes the visual emphasis. +// Renders a single primary "Download for macOS" button (macOS is the +// server-side default) plus a quiet one-line row of the remaining +// platforms. Client-side detection (src/scripts/main.js) rewrites the +// primary button's label/sub/href to match the visitor's platform and +// swaps the detected platform out of the alt row. // -// Download sources (resolved at runtime — never bake versions in): -// • macOS + Windows + Linux → GitHub Releases (latest) -// • iOS / iPadOS → TestFlight public link. -// (The interactive "coven cli" card in the hero already demos the CLI, so the -// CTA stays focused on native downloads rather than repeating an install line.) +// Works fully without JS: the primary button and every alt-row link are +// real s pointing at the canonical release page or TestFlight. // -// Latest releases live on: -// https://github.com/OpenCoven/coven-cave/releases/latest -// We link to /releases/latest (the release page) for the platform buttons -// because asset filenames carry a version suffix (e.g. CovenCave-vX.Y.Z-...) -// and GitHub's /latest/download/ redirect only works with stable -// filenames. The release page already lists every artifact in one tap and -// ships the user straight to the newest signed build — no version drift in source. -// build — no version drift in source. +// Download sources (resolved at runtime — never bake versions in): +// • macOS + Windows + Linux → GitHub Releases (latest release page; +// asset filenames carry version suffixes so /latest/download/ +// redirects can't be used). +// • iOS / iPadOS → TestFlight public link. const RELEASES_URL = 'https://github.com/OpenCoven/coven-cave/releases/latest'; const TESTFLIGHT_URL = 'https://testflight.apple.com/join/61Dqw8y4'; -// Inline icon helpers — kept tiny so the hero stays the LCP. -const ICONS = { - apple: ``, - ios: ``, - windows: ``, - linux: ``, -}; - type Platform = 'mac' | 'ios' | 'win' | 'linux'; -interface Btn { +interface Alt { id: Platform; label: string; - sub: string; href: string; - icon: string; + external?: boolean; } -const buttons: Btn[] = [ - { id: 'mac', label: 'macOS', sub: '.dmg · universal · signed', href: RELEASES_URL, icon: ICONS.apple }, - { id: 'win', label: 'Windows', sub: '.msi · x64 · signed', href: RELEASES_URL, icon: ICONS.windows }, - { id: 'linux', label: 'Linux', sub: '.AppImage · x86_64', href: RELEASES_URL, icon: ICONS.linux }, - { id: 'ios', label: 'iOS Beta', sub: 'TestFlight · iPhone & iPad', href: TESTFLIGHT_URL, icon: ICONS.ios }, +const alts: Alt[] = [ + { id: 'win', label: 'Windows', href: RELEASES_URL }, + { id: 'linux', label: 'Linux', href: RELEASES_URL }, + { id: 'ios', label: 'iOS (TestFlight)', href: TESTFLIGHT_URL, external: true }, ]; --- -
- - - Public beta · free · open source - - -

- Download - CovenCave -

- -

- Native apps and the CLI — your platform's build is highlighted. -

+
+ + + + Download for macOS + CovenCave · .dmg · signed · free + + -
- {buttons.map((b) => ( +

+ also on + {alts.map((a) => ( - - - {b.label} - {b.sub} - - - + class="download-alt" + href={a.href} + data-alt-platform={a.id} + target={a.external ? '_blank' : null} + rel={a.external ? 'noopener noreferrer' : null} + >{a.label} ))} -

- - - All releases & checksums ↗ - + all releases ↗ +

diff --git a/src/components/Hero.astro b/src/components/Hero.astro index f0c9deb..eb2b4bd 100644 --- a/src/components/Hero.astro +++ b/src/components/Hero.astro @@ -1,118 +1,122 @@ --- -// Hero section (Layout C, no-scroll): a centered vertical stack. -// Row 1 (.hero-main): copy + platform-aware DownloadCTA on the LEFT, -// interactive "Coven CLI" runtime card on the RIGHT. -// Row 2 (.feature-grid): a slim full-width strip of the 4 core capabilities. -// Splitting the feature grid out of the copy column keeps either side from -// growing tall enough to force scrolling. Auto-rotation and CLI typing are -// driven by src/scripts/main.js. +// Hero — "the summoning". // -// The primary CTA is the platform-aware DownloadCTA (auto-detects macOS / -// iOS / Windows / Linux) — Discord/Docs are kept as low-emphasis secondary -// actions. +// Three layers, one thesis (continuity): +// 1. .hero-main — copy + platform-aware DownloadCTA on the LEFT, +// a live "summon session" terminal on the RIGHT. The +// terminal reads top-to-bottom like a real session: +// typed command → familiar state as output → roster tabs. +// 2. .continuity — the signature element: a thin timeline across the +// hero's base plotting one familiar's memory across real +// time, ending in a live "today · still remembers" pulse. +// It replaces the old 4-cell icon grid (whose content +// duplicated ProofGrid) and encodes persistence itself. +// +// Terminal rotation + typing + roster switching live in src/scripts/main.js. +// Everything renders complete without JS; scripts only add motion/emphasis. import DownloadCTA from './DownloadCTA.astro'; ---
-

OPENCOVEN · PERSISTENT AI FAMILIARS

-

Where AI Meets
Continuity.

+

+ public beta + free & open source · runs locally +

+ +

Summon agents
that remember.

+

- Named familiars with memory, tools, identity, and continuity — - running locally, inside your projects, on your terms. + Coven gives each AI agent a name, a memory, and a place in your work. + Close the terminal tonight — tomorrow Forge still knows + your branch, your conventions, and where you left off.

-
-
+
-
- coven cli - local · active + + coven · session + local +
+ +
+ + coven attach forge --project ./opencoven +
-
-
diff --git a/src/pages/index.astro b/src/pages/index.astro index 102f4aa..28be2aa 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -5,6 +5,9 @@ // immediately and re-flows when the font arrives. import '@fontsource-variable/inter/wght.css'; import '@fontsource-variable/jetbrains-mono/wght.css'; +// Fraunces italic is the hero's display voice (one word in the h1); the +// italic-only variable file keeps the added font cost to a single axis. +import '@fontsource-variable/fraunces/wght-italic.css'; import '../styles/global.css'; import Ambient from '../components/Ambient.astro'; diff --git a/src/scripts/main.js b/src/scripts/main.js index bc9fed7..e72ca2c 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -58,20 +58,26 @@ }, { passive: true }); })(); - // ── Hero card: live familiar state ──────────────────────── + // ── Hero terminal: live summon session ──────────────────── (function () { - var rows = document.querySelectorAll('.card-familiar[data-familiar]'); - if (rows.length === 0) return; - var card = document.querySelector('.hero-card'); - var memBody = document.querySelector('.hero-card-memory'); - var memTitle = document.querySelector('.memory-title'); - var memNotes = document.querySelectorAll('.memory-note'); - var cliCmd = document.querySelector('.card-cmd'); - var motionOn = document.documentElement.classList.contains('motion-on'); + var tabs = document.querySelectorAll('.roster-tab[data-familiar]'); + if (tabs.length === 0) return; + var card = document.querySelector('.hero-card'); + var output = document.querySelector('.hero-card-output'); + var sigilEl = document.querySelector('[data-sigil]'); + var nameEl = document.querySelector('[data-name]'); + var roleEl = document.querySelector('[data-role]'); + var countEl = document.querySelector('[data-count]'); + var memNotes = document.querySelectorAll('.memory-note'); + var cliCmd = document.querySelector('.card-cmd'); + var motionOn = document.documentElement.classList.contains('motion-on'); var FAMILIARS = { forge: { name: 'Forge', + sigil: 'F', + role: 'code steward · tools · git', + count: '128 notes · 47 days', command: 'coven attach forge --project ./opencoven', notes: [ { text: 'resumed feat/runtime-attach · 4 files staged', meta: '2h ago' }, @@ -80,6 +86,9 @@ }, charm: { name: 'Charm', + sigil: 'C', + role: 'voice · social · presence', + count: '64 notes · 21 days', command: 'coven attach charm --voice on --thread design-sync', notes: [ { text: 'drafted reply for #design-sync · awaiting review', meta: '12m ago' }, @@ -88,6 +97,9 @@ }, sage: { name: 'Sage', + sigil: 'S', + role: 'research · docs · long context', + count: '203 notes · 65 days', command: 'coven attach sage --context ./docs --long', notes: [ { text: 'loaded 4 docs into context · 18k tokens', meta: '40m ago' }, @@ -101,13 +113,6 @@ var rotateTimer = null; var typeTimer = null; - function setBadge(badge, isActive) { - if (!badge) return; - badge.textContent = isActive ? 'active' : 'idle'; - badge.classList.toggle('badge-active', isActive); - badge.classList.toggle('badge-idle', !isActive); - } - function typeCommand(cmd) { if (typeTimer) { clearTimeout(typeTimer); typeTimer = null; } if (!cliCmd) return; @@ -122,8 +127,11 @@ step(); } - function paintNotes(data) { - if (memTitle) memTitle.textContent = 'memory · ' + data.name.toLowerCase(); + function paintOutput(data) { + if (sigilEl) sigilEl.textContent = data.sigil; + if (nameEl) nameEl.textContent = data.name; + if (roleEl) roleEl.textContent = data.role; + if (countEl) countEl.textContent = data.count; for (var i = 0; i < memNotes.length; i++) { var note = data.notes[i]; if (!note) continue; @@ -137,30 +145,28 @@ function applyFamiliar(id, animate) { if (!FAMILIARS[id]) return; current = id; - rows.forEach(function (row) { - var isActive = row.getAttribute('data-familiar') === id; - row.classList.toggle('active', isActive); - row.setAttribute('aria-selected', isActive ? 'true' : 'false'); - row.setAttribute('tabindex', isActive ? '0' : '-1'); - setBadge(row.querySelector('.familiar-badge'), isActive); + tabs.forEach(function (tab) { + var isActive = tab.getAttribute('data-familiar') === id; + tab.classList.toggle('active', isActive); + tab.setAttribute('aria-selected', isActive ? 'true' : 'false'); + tab.setAttribute('tabindex', isActive ? '0' : '-1'); }); var data = FAMILIARS[id]; - if (animate && memBody && motionOn) { - memBody.classList.add('memory-swap'); + if (animate && output && motionOn) { + output.classList.add('memory-swap'); setTimeout(function () { - paintNotes(data); - memBody.classList.remove('memory-swap'); + paintOutput(data); + output.classList.remove('memory-swap'); }, 200); } else { - paintNotes(data); + paintOutput(data); } typeCommand(data.command); } function rotate() { var idx = order.indexOf(current); - var next = order[(idx + 1) % order.length]; - applyFamiliar(next, true); + applyFamiliar(order[(idx + 1) % order.length], true); } function startRotate() { if (userTook) return; @@ -171,19 +177,22 @@ if (rotateTimer) { clearInterval(rotateTimer); rotateTimer = null; } } - rows.forEach(function (row) { - row.addEventListener('click', function () { + tabs.forEach(function (tab, i) { + tab.addEventListener('click', function () { userTook = true; stopRotate(); - applyFamiliar(row.getAttribute('data-familiar'), true); + applyFamiliar(tab.getAttribute('data-familiar'), true); }); - row.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - userTook = true; - stopRotate(); - applyFamiliar(row.getAttribute('data-familiar'), true); - } + // Roving tabindex + arrow keys (standard tablist keyboard pattern) + tab.addEventListener('keydown', function (e) { + var dir = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; + if (!dir) return; + e.preventDefault(); + var next = tabs[(i + dir + tabs.length) % tabs.length]; + userTook = true; + stopRotate(); + applyFamiliar(next.getAttribute('data-familiar'), true); + next.focus(); }); }); @@ -223,10 +232,10 @@ // ── Download CTA: detect visitor's platform ───────────────── // - // Promotes the matching button to the visually-primary slot and - // updates the headline. Works fully without JS — every link is - // a real with the canonical destination — this just - // adds visual emphasis. + // Rewrites the single primary button's label/sub/href to match the + // visitor's platform and hides that platform from the "also on" row. + // Works fully without JS — the primary defaults to macOS and every + // alt link is a real — this only retargets the emphasis. // // Detection order: // 1. UA-string OS markers (most intentional; matches UA overrides) @@ -237,10 +246,13 @@ var cta = document.querySelector('[data-download-cta]'); if (!cta) return; - var headingEl = cta.querySelector('[data-download-platform]'); + var primary = cta.querySelector('[data-download-primary]'); + var labelEl = cta.querySelector('[data-download-label]'); var subEl = cta.querySelector('[data-download-sub]'); - var buttons = cta.querySelectorAll('.download-btn'); - if (!buttons.length) return; + if (!primary) return; + + var releasesUrl = cta.getAttribute('data-releases-url'); + var testflightUrl = cta.getAttribute('data-testflight-url'); var ua = navigator.userAgent || ''; var rawPlatform = (navigator.platform || '').toLowerCase(); @@ -285,31 +297,37 @@ } var COPY = { - mac: { name: 'CovenCave for macOS', sub: "We detected macOS — your one-click download is highlighted below." }, - ios: { name: 'CovenCave for iPhone & iPad', sub: "We detected an iOS device — join the TestFlight beta below." }, - win: { name: 'CovenCave for Windows', sub: "We detected Windows — your one-click installer is highlighted below." }, - linux: { name: 'CovenCave for Linux', sub: "We detected Linux — your one-click AppImage is highlighted below." }, + mac: { label: 'Download for macOS', sub: 'CovenCave · .dmg · signed · free', href: releasesUrl }, + win: { label: 'Download for Windows', sub: 'CovenCave · .msi · signed · free', href: releasesUrl }, + linux: { label: 'Download for Linux', sub: 'CovenCave · .AppImage · x86_64 · free', href: releasesUrl }, + ios: { label: 'Get the iOS beta', sub: 'CovenCave · TestFlight · iPhone & iPad', href: testflightUrl }, }; var copy = COPY[detected] || COPY.mac; - if (headingEl) headingEl.textContent = copy.name; + if (labelEl) labelEl.textContent = copy.label; if (subEl) subEl.textContent = copy.sub; + if (copy.href) primary.setAttribute('href', copy.href); + primary.setAttribute('data-platform', detected); + if (detected === 'ios') { + primary.setAttribute('target', '_blank'); + primary.setAttribute('rel', 'noopener noreferrer'); + } - // Promote the matching button to .is-primary. Desktop platforms move to - // the top; TestFlight stays in the bottom full-width row. - var primary = null; - buttons.forEach(function (btn) { - btn.classList.remove('is-primary'); - if (btn.getAttribute('data-platform') === detected) { - primary = btn; - } + // Drop the detected platform from the "also on" row (it's now the + // primary) and re-list macOS in its place when the visitor isn't + // on a Mac, so all four platforms stay one click away. + cta.querySelectorAll('.download-alt[data-alt-platform]').forEach(function (a) { + a.classList.toggle('is-detected', a.getAttribute('data-alt-platform') === detected); }); - - if (primary && detected !== 'ios') { - // Emphasize the detected platform in place (violet fill). We no longer - // move it or span it full-width — the download row stays a stable - // single line of four so the hero height is predictable (no-scroll). - primary.classList.add('is-primary'); + if (detected !== 'mac' && releasesUrl) { + var macAlt = document.createElement('a'); + macAlt.className = 'download-alt'; + macAlt.setAttribute('data-alt-platform', 'mac'); + macAlt.href = releasesUrl; + macAlt.textContent = 'macOS'; + var altsRow = cta.querySelector('.download-alts'); + var kicker = altsRow && altsRow.querySelector('.download-alts-kicker'); + if (kicker) kicker.insertAdjacentElement('afterend', macAlt); } // Stamp the resolved platform on the cta for analytics / debug. diff --git a/src/styles/global.css b/src/styles/global.css index b5e25f1..631e37d 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -55,6 +55,9 @@ /* ── Typography (self-hosted Inter Variable + JetBrains Mono Variable) ── */ --font-ui: 'Inter Variable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --mono: 'JetBrains Mono Variable', 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace; + /* Display voice — used only for the emphasized word in the hero h1. + Fraunces italic: the grimoire serif against the hard grotesque. */ + --font-display: 'Fraunces Variable', 'Fraunces', Georgia, 'Times New Roman', serif; } /* Screen-reader-only utility — used for invisible landmark headings @@ -229,116 +232,102 @@ color: var(--white); padding: 8px; } - /* ── HERO ────────────────────────────────────────────── */ - /* Layout C: a single no-scroll viewport. A centered vertical stack holds - the 2-column main row (copy+downloads LEFT, CLI card RIGHT) with a slim - full-width feature strip beneath it, so neither column grows tall enough - to force scrolling. */ + /* ── HERO — "the summoning" ──────────────────────────── */ + /* A single no-scroll viewport: the 2-column main row (copy + downloads + LEFT, live summon-session terminal RIGHT) with the continuity + timeline — the hero's signature — threaded across its base. */ .hero { position: relative; z-index: 1; - /* Fit the whole hero within one viewport minus the fixed header (~60px), - with a little bottom breathing room so the feature strip is never - flush against the fold on short 720px laptops. */ + /* Fit within one viewport minus the sticky header (~60px), with + breathing room so the continuity strip never kisses the fold. */ min-height: calc(100vh - 72px); display: flex; flex-direction: column; justify-content: center; - gap: 20px; + gap: 44px; max-width: 1280px; margin: 0 auto; - padding: 22px 32px 30px; + padding: 30px 32px 38px; } /* The 2-column main row */ .hero-main { display: grid; - grid-template-columns: 1.1fr 0.9fr; + grid-template-columns: 1.08fr 0.92fr; align-items: center; - gap: 56px; + gap: 64px; } + /* Both columns hold nowrap terminal text (.card-cmd, memory notes); + min-width: 0 lets them shrink instead of forcing page overflow. */ + .hero-main > * { min-width: 0; } .hero-copy { display: flex; flex-direction: column; justify-content: center; - gap: 0; } .kicker { font-family: var(--mono); - font-size: 10px; font-weight: 600; - letter-spacing: 0.18em; - color: var(--vtext); opacity: 0.7; - margin-bottom: 18px; - display: flex; align-items: center; gap: 10px; + font-size: 11px; font-weight: 500; + letter-spacing: 0.1em; + color: var(--muted); + margin-bottom: 22px; + display: flex; align-items: center; flex-wrap: wrap; gap: 12px; } - .kicker::before { - content: ''; - display: inline-block; width: 20px; height: 1px; - background: var(--violet); opacity: 0.6; + .kicker-beta { + display: inline-flex; align-items: center; gap: 7px; + padding: 4px 11px; + border-radius: 999px; + border: 1px solid rgba(154,142,205,0.35); + background: rgba(154,142,205,0.08); + color: var(--vtext); + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + font-size: 9.5px; + } + .beta-dot { + width: 6px; height: 6px; border-radius: 999px; + background: var(--green); + box-shadow: 0 0 10px rgba(48,209,88,0.7); + animation: beta-pulse 2.4s ease-in-out infinite; + } + @keyframes beta-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } } h1 { - font-size: clamp(40px, 4.6vw, 62px); - font-weight: 900; - line-height: 1.02; - letter-spacing: -0.04em; + font-size: clamp(42px, 4.9vw, 68px); + font-weight: 850; + line-height: 1.04; + letter-spacing: -0.038em; color: var(--white); - margin-bottom: 22px; + margin-bottom: 24px; } + /* The one display-type moment on the page: the differentiator set in a + grimoire serif italic against the hard grotesque around it. */ h1 em { - font-style: normal; + font-family: var(--font-display); + font-style: italic; + font-weight: 460; + font-size: 1.05em; + letter-spacing: -0.015em; color: var(--vtext); + text-shadow: 0 0 44px rgba(154,142,205,0.35); } .hero-lede { - font-size: 16px; line-height: 1.55; + font-size: 16.5px; line-height: 1.6; color: var(--muted); - max-width: 460px; - margin-bottom: 22px; - } - - /* 4-column mini feature grid — a slim full-width strip beneath the main row */ - .feature-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1px; - background: var(--border); - border: 1px solid var(--border); - border-radius: 8px; - overflow: hidden; - margin-bottom: 0; - } - .feature-cell { - background: var(--bg); - padding: 14px 16px 15px; - display: flex; flex-direction: column; gap: 6px; - position: relative; - transition: background 0.25s ease; - } - .feature-icon { - font-size: 18px; line-height: 1; - margin-bottom: 2px; - display: inline-block; - transition: transform 0.25s ease, color 0.25s ease; - color: var(--vtext); - opacity: 0.85; + max-width: 500px; + margin-bottom: 30px; } - .feature-label { - font-family: var(--mono); - font-size: 9px; font-weight: 700; - letter-spacing: 0.14em; text-transform: uppercase; - color: var(--vtext); - transition: color 0.25s ease; - } - .feature-desc { - font-size: 11px; line-height: 1.5; - color: var(--muted); + .hero-lede strong { + color: var(--white); + font-weight: 600; } - /* CTAs */ - .hero-actions { - display: flex; align-items: center; gap: 12px; - flex-wrap: wrap; - } + /* Buttons shared with QuickStart (.qs-actions) */ .btn-primary { display: inline-flex; align-items: center; gap: 8px; background: var(--violet-cta); @@ -363,31 +352,119 @@ } .btn-secondary:hover { color: var(--white); border-color: rgba(142,61,255,0.4); } - /* Hero visual — right column (field-manual terminal card) */ + /* ── Download CTA — one decision, not four ───────────── + A single primary button (server-default: macOS) that main.js + rewrites for the detected platform, plus a quiet mono line of + the remaining platforms. */ + .download-cta { + display: flex; flex-direction: column; + gap: 14px; + align-items: flex-start; + } + .download-primary { + display: inline-flex; align-items: center; gap: 14px; + text-decoration: none; + padding: 14px 26px 14px 20px; + border-radius: 12px; + background: linear-gradient(135deg, var(--violet-cta) 0%, #55409b 100%); + border: 1px solid rgba(255,255,255,0.14); + color: #fff; + box-shadow: + 0 12px 32px -10px rgba(109,91,175,0.7), + 0 0 0 1px rgba(154,142,205,0.18); + transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; + } + .download-primary:hover { + transform: translateY(-2px); + background: linear-gradient(135deg, #7e6cc4 0%, #5e479e 100%); + box-shadow: + 0 18px 40px -10px rgba(109,91,175,0.85), + 0 0 0 1px rgba(180,170,235,0.32); + } + .download-primary:active { transform: translateY(0); } + .download-primary-icon { + width: 22px; height: 22px; + flex: 0 0 auto; + color: rgba(255,255,255,0.92); + } + .download-primary-text { + display: flex; flex-direction: column; gap: 3px; + text-align: left; + } + .download-primary-label { + font-size: 15px; font-weight: 700; + letter-spacing: -0.01em; + line-height: 1.15; + } + .download-primary-sub { + font-family: var(--mono); + font-size: 10px; font-weight: 500; + letter-spacing: 0.06em; + color: rgba(255,255,255,0.72); + } + .download-alts { + display: flex; align-items: baseline; flex-wrap: wrap; + gap: 6px 16px; + font-family: var(--mono); + font-size: 11.5px; + letter-spacing: 0.03em; + margin: 0; + } + .download-alts-kicker { color: var(--muted); } + .download-alt { + color: var(--muted); + text-decoration: none; + border-bottom: 1px dotted rgba(154,142,205,0.4); + padding-bottom: 1px; + transition: color 0.15s, border-color 0.15s; + } + .download-alt:hover { color: var(--vtext); border-bottom-color: var(--vtext); } + .download-alt.is-detected { display: none; } + + /* Secondary hero actions (Discord + Docs) — deliberately low-key */ + .hero-actions-secondary { + display: flex; align-items: center; gap: 10px; flex-wrap: wrap; + margin-top: 22px; + } + .btn-ghost { + display: inline-flex; align-items: center; gap: 8px; + background: transparent; + color: var(--muted); text-decoration: none; + font-size: 12px; font-weight: 600; + letter-spacing: 0.04em; + padding: 8px 14px; border-radius: 6px; + transition: color 0.15s; + } + .btn-ghost:hover { color: var(--white); } + .btn-ghost svg { width: 14px; height: 11px; fill: currentColor; } + + /* ── Hero terminal — a live summon session ───────────── + Reads top-to-bottom like a real session: typed command → + familiar state as output → roster tabs. Rotation, typing and + tab switching are driven by src/scripts/main.js. */ .hero-visual { position: relative; - /* Swapped hero: the Coven CLI card now leads on the LEFT, so its inner - gutter is on the right (toward the copy). */ - order: 1; display: flex; align-items: center; justify-content: center; - padding: 40px 40px 40px 0; } .hero-card { width: 100%; - max-width: 480px; - background: rgba(17,16,24,0.72); - border: 1px solid rgba(142,61,255,0.18); - border-radius: 10px; + max-width: 500px; + background: rgba(17,16,24,0.78); + border: 1px solid rgba(154,142,205,0.22); + border-radius: 14px; overflow: hidden; font-family: var(--mono); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - box-shadow: 0 0 0 1px rgba(142,61,255,0.06), 0 24px 64px rgba(0,0,0,0.45); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + box-shadow: + 0 0 0 1px rgba(154,142,205,0.05), + 0 32px 80px rgba(0,0,0,0.5), + 0 0 90px -30px rgba(154,142,205,0.28); } .hero-card-topbar { display: flex; align-items: center; justify-content: space-between; - padding: 10px 16px; - border-bottom: 1px solid rgba(142,61,255,0.12); + padding: 11px 18px; + border-bottom: 1px solid rgba(154,142,205,0.14); background: rgba(5,4,9,0.6); } .hero-card-dots { display: flex; gap: 6px; } @@ -398,129 +475,253 @@ } .hero-card-label { font-size: 9px; font-weight: 700; - letter-spacing: 0.14em; text-transform: uppercase; - color: rgba(201,167,255,0.5); + letter-spacing: 0.16em; text-transform: uppercase; + color: rgba(196,185,240,0.55); } .hero-card-status { display: flex; align-items: center; gap: 6px; - font-size: 9px; color: rgba(255,255,255,0.3); - letter-spacing: 0.08em; + font-size: 9px; color: rgba(255,255,255,0.35); + letter-spacing: 0.1em; text-transform: uppercase; } .hero-card-status::before { content: ''; width: 6px; height: 6px; border-radius: 50%; - background: #4ade80; - box-shadow: 0 0 6px rgba(74,222,128,0.5); + background: var(--green); + box-shadow: 0 0 6px rgba(48,209,88,0.5); } - .hero-card-body { padding: 20px; display: flex; flex-direction: column; gap: 0; } - .card-familiar { - display: grid; - grid-template-columns: 32px 1fr auto; - align-items: center; - gap: 12px; - padding: 12px 0; - border-bottom: 1px solid rgba(255,255,255,0.05); + + .hero-card-cmdline { + display: flex; align-items: center; gap: 9px; + padding: 15px 20px 13px; } - .card-familiar:last-child { border-bottom: none; } - .card-familiar { outline: none; user-select: none; } - .card-familiar:focus-visible { - box-shadow: inset 0 0 0 1px rgba(142,61,255,0.5); - border-radius: 4px; + .card-prompt { font-size: 12px; color: var(--vtext); flex-shrink: 0; } + .card-cmd { + flex: 1 1 auto; min-width: 0; + font-size: 12px; color: rgba(232,224,240,0.88); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + transition: opacity 0.2s ease; } - .card-familiar.active .familiar-sigil { - border-color: rgba(142,61,255,0.55); - background: rgba(142,61,255,0.22); - color: #E8DBFF; - box-shadow: 0 0 0 1px rgba(142,61,255,0.15), 0 0 14px rgba(142,61,255,0.18); + .card-cursor { + width: 7px; height: 14px; + background: rgba(154,142,205,0.85); + border-radius: 1px; + animation: blink 1.1s step-end infinite; + flex-shrink: 0; + } + @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} } + + .hero-card-output { + margin: 0 20px 16px; + border: 1px solid rgba(154,142,205,0.14); + border-radius: 10px; + overflow: hidden; + background: rgba(5,4,9,0.35); + } + .output-identity { + display: grid; + grid-template-columns: 38px 1fr auto; + align-items: center; + gap: 13px; + padding: 14px 16px; } - .card-familiar.active .familiar-name { color: #fff; } - /* Memory note swap fade — applied by JS when notes change */ - .memory-note, .memory-title, .card-cmd { transition: opacity 0.2s ease; } - .memory-swap .memory-note, - .memory-swap .memory-title { opacity: 0.15; } - .familiar-badge { transition: background 0.25s ease, color 0.25s ease, border-color 0.25s ease; } .familiar-sigil { - width: 32px; height: 32px; - border-radius: 6px; - border: 1px solid rgba(142,61,255,0.28); - background: rgba(142,61,255,0.1); + width: 38px; height: 38px; + border-radius: 9px; + border: 1px solid rgba(154,142,205,0.5); + background: linear-gradient(150deg, rgba(154,142,205,0.28), rgba(154,142,205,0.08)); display: grid; place-items: center; - font-size: 11px; font-weight: 800; - color: rgba(201,167,255,0.9); - transition: background 0.2s, border-color 0.2s; + font-size: 14px; font-weight: 800; + color: #E8E0F0; + box-shadow: 0 0 18px rgba(154,142,205,0.22); + transition: opacity 0.2s ease; + } + .familiar-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; } + .familiar-name { + font-size: 13px; font-weight: 700; + color: #fff; letter-spacing: 0.02em; + transition: opacity 0.2s ease; + } + .familiar-role { + font-size: 9px; color: rgba(232,224,240,0.52); + letter-spacing: 0.08em; text-transform: uppercase; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + transition: opacity 0.2s ease; } - .familiar-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; } - .familiar-name { font-size: 11px; font-weight: 700; color: rgba(255,255,255,0.9); letter-spacing: 0.02em; } - .familiar-role { font-size: 9px; color: rgba(255,255,255,0.35); letter-spacing: 0.06em; text-transform: uppercase; } .familiar-badge { - font-size: 8px; font-weight: 700; - letter-spacing: 0.1em; text-transform: uppercase; - padding: 3px 8px; border-radius: 4px; + font-size: 8.5px; font-weight: 700; + letter-spacing: 0.12em; text-transform: uppercase; + padding: 4px 9px; border-radius: 5px; + } + .badge-active { + background: rgba(48,209,88,0.1); color: rgba(48,209,88,0.85); + border: 1px solid rgba(48,209,88,0.22); } - .badge-active { background: rgba(74,222,128,0.1); color: rgba(74,222,128,0.8); border: 1px solid rgba(74,222,128,0.2); } - .badge-idle { background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.3); border: 1px solid rgba(255,255,255,0.08); } - .hero-card-memory { - padding: 14px 20px 16px; - border-top: 1px solid rgba(142,61,255,0.12); - background: linear-gradient(180deg, rgba(142,61,255,0.04) 0%, rgba(142,61,255,0.015) 100%); - display: flex; flex-direction: column; gap: 7px; + .output-memory { + padding: 13px 16px 15px; + border-top: 1px solid rgba(154,142,205,0.12); + background: linear-gradient(180deg, rgba(154,142,205,0.05) 0%, rgba(154,142,205,0.015) 100%); + display: flex; flex-direction: column; gap: 8px; } .memory-header { font-size: 9px; font-weight: 700; - letter-spacing: 0.14em; text-transform: uppercase; - color: rgba(201,167,255,0.55); + letter-spacing: 0.15em; text-transform: uppercase; + color: rgba(196,185,240,0.6); display: flex; align-items: center; gap: 8px; - margin-bottom: 4px; + margin-bottom: 2px; } .memory-header::before { content: ''; display: inline-block; width: 5px; height: 5px; border-radius: 50%; - background: rgba(201,167,255,0.55); - box-shadow: 0 0 6px rgba(201,167,255,0.4); + background: rgba(196,185,240,0.6); + box-shadow: 0 0 6px rgba(196,185,240,0.45); } .memory-header .memory-count { margin-left: auto; - color: rgba(255,255,255,0.25); + color: rgba(255,255,255,0.3); font-weight: 500; - letter-spacing: 0.1em; + letter-spacing: 0.08em; + text-transform: none; } .memory-note { display: flex; align-items: baseline; gap: 8px; font-size: 10.5px; - color: rgba(255,255,255,0.6); - letter-spacing: 0.005em; - line-height: 1.4; + color: rgba(232,224,240,0.62); + line-height: 1.45; + transition: opacity 0.2s ease; } .memory-note::before { content: '◇'; - color: rgba(201,167,255,0.55); + color: rgba(196,185,240,0.55); font-size: 8.5px; flex-shrink: 0; transform: translateY(-1px); } .memory-note .memory-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .memory-note .memory-meta { - color: rgba(255,255,255,0.28); + color: rgba(232,224,240,0.45); font-size: 9px; letter-spacing: 0.06em; flex-shrink: 0; } - .hero-card-footer { - padding: 12px 20px; - border-top: 1px solid rgba(142,61,255,0.1); + .memory-title { transition: opacity 0.2s ease; } + /* Output swap fade — applied by JS when the familiar changes */ + .memory-swap .memory-note, + .memory-swap .memory-title, + .memory-swap .familiar-sigil, + .memory-swap .familiar-name, + .memory-swap .familiar-role { opacity: 0.12; } + + .hero-card-roster { + display: flex; align-items: center; gap: 6px; + padding: 12px 20px 14px; + border-top: 1px solid rgba(154,142,205,0.12); background: rgba(5,4,9,0.4); - display: flex; align-items: center; gap: 8px; } - .card-prompt { font-size: 10px; color: rgba(201,167,255,0.5); flex-shrink: 0; } - .card-cmd { font-size: 10px; color: rgba(255,255,255,0.6); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .card-cursor { - width: 6px; height: 12px; - background: rgba(142,61,255,0.8); - border-radius: 1px; - animation: blink 1.1s step-end infinite; - flex-shrink: 0; + .roster-tab { + font-family: var(--mono); + font-size: 10.5px; font-weight: 600; + letter-spacing: 0.04em; + color: var(--muted); + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + padding: 5px 12px; + cursor: pointer; + transition: color 0.18s, background 0.18s, border-color 0.18s; + } + .roster-tab:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(154,142,205,0.55); + } + .roster-tab.active { + color: #fff; + background: rgba(154,142,205,0.16); + border-color: rgba(154,142,205,0.4); + } + .roster-hint { + margin-left: auto; + min-width: 0; + text-align: right; + font-size: 9px; + color: rgba(232,224,240,0.45); + letter-spacing: 0.08em; + } + + /* ── Continuity timeline — the signature ─────────────── + One familiar's memory plotted across real time; the thread ends + in a live "today" node. Persistence, drawn instead of claimed. */ + .continuity { + position: relative; + padding-top: 6px; + } + .continuity-track { + position: absolute; + left: 0; + /* End the thread at the "today" node instead of running past it: + trim the width of the last grid column minus its node radius + (columns are (100% - 3×24px gap) / 4 wide — see .continuity-events). */ + right: calc((100% - 72px) / 4 - 9px); + top: calc(6px + 14px + 10px + 4px); /* pad + date row + gap + node center */ + height: 1px; + background: linear-gradient(90deg, + rgba(154,142,205,0.08) 0%, + rgba(154,142,205,0.35) 55%, + rgba(180,170,235,0.75) 100%); + pointer-events: none; + } + .continuity-events { + list-style: none; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; + } + .continuity-event { + display: grid; + grid-template-rows: 14px 9px auto; + row-gap: 10px; + justify-items: start; + } + .continuity-date { + font-family: var(--mono); + font-size: 9.5px; font-weight: 600; + line-height: 14px; + letter-spacing: 0.14em; text-transform: uppercase; + color: var(--muted); /* AA at small sizes — matches body-muted token */ + } + .continuity-node { + width: 9px; height: 9px; + border-radius: 50%; + background: var(--oc-surface-1); + border: 1.5px solid rgba(154,142,205,0.65); + box-shadow: 0 0 0 3px rgba(5,4,9,0.9); + position: relative; z-index: 1; + } + .continuity-text { + font-size: 12px; line-height: 1.5; + color: var(--muted); + max-width: 24ch; + } + .continuity-event.is-now .continuity-date { color: var(--vtext); } + .continuity-event.is-now .continuity-text { color: var(--oc-text); } + .continuity-event.is-now .continuity-node { + background: var(--oc-purple-light); + border-color: var(--oc-purple-light); + box-shadow: + 0 0 0 3px rgba(5,4,9,0.9), + 0 0 14px rgba(180,170,235,0.8); + } + .motion-on .continuity-event.is-now .continuity-node::after { + content: ''; + position: absolute; inset: -5px; + border-radius: 50%; + border: 1px solid rgba(180,170,235,0.7); + animation: now-pulse 2.6s ease-out infinite; + } + @keyframes now-pulse { + 0% { transform: scale(0.6); opacity: 0.9; } + 70% { transform: scale(1.6); opacity: 0; } + 100% { transform: scale(1.6); opacity: 0; } } - @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} } /* ── TESTIMONIAL STRIP ───────────────────────────────── */ .testimonial-strip { @@ -809,25 +1010,19 @@ /* Hover states isolated to (hover: hover) so touch devices don't latch sticky :hover state after a tap. Same easing/timing everywhere. */ @media (hover: hover) { - .feature-cell:hover, .proof-card:hover, .glass-card:hover { background: linear-gradient(180deg, rgba(142,61,255,0.07) 0%, var(--surface) 100%); } - .feature-cell:hover .feature-icon { - transform: scale(1.12); - color: #E8DBFF; - } - .feature-cell:hover .feature-label { color: #fff; } .proof-card:hover .proof-num { opacity: 1; color: var(--vtext); } .proof-card:hover h3 { color: var(--vtext); } .glass-card:hover .card-kicker { opacity: 1; color: var(--vtext); } .glass-card:hover h3 { color: #fff; } - /* Hero card familiar rows: subtle hover hint */ - .card-familiar { transition: background 0.2s ease; cursor: pointer; } - .card-familiar:hover:not(.active) { - background: rgba(142,61,255,0.04); + /* Hero roster tabs: subtle hover hint */ + .roster-tab:hover:not(.active) { + color: var(--white); + background: rgba(154,142,205,0.07); } } @@ -847,15 +1042,15 @@ transform: none; } /* Stagger items within grids — same timing across every grid */ - .motion-on .feature-grid [data-reveal]:nth-child(2), - .motion-on .proof-grid [data-reveal]:nth-child(2), - .motion-on .card-grid [data-reveal]:nth-child(2) { transition-delay: 0.08s; } - .motion-on .feature-grid [data-reveal]:nth-child(3), - .motion-on .proof-grid [data-reveal]:nth-child(3), - .motion-on .card-grid [data-reveal]:nth-child(3) { transition-delay: 0.16s; } - .motion-on .feature-grid [data-reveal]:nth-child(4), - .motion-on .proof-grid [data-reveal]:nth-child(4), - .motion-on .card-grid [data-reveal]:nth-child(4) { transition-delay: 0.24s; } + .motion-on .continuity-events [data-reveal]:nth-child(2), + .motion-on .proof-grid [data-reveal]:nth-child(2), + .motion-on .card-grid [data-reveal]:nth-child(2) { transition-delay: 0.08s; } + .motion-on .continuity-events [data-reveal]:nth-child(3), + .motion-on .proof-grid [data-reveal]:nth-child(3), + .motion-on .card-grid [data-reveal]:nth-child(3) { transition-delay: 0.16s; } + .motion-on .continuity-events [data-reveal]:nth-child(4), + .motion-on .proof-grid [data-reveal]:nth-child(4), + .motion-on .card-grid [data-reveal]:nth-child(4) { transition-delay: 0.24s; } /* ── REDUCED MOTION ──────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { @@ -913,8 +1108,35 @@ .hero-visual { order: 2; justify-content: flex-start; } .hero-card { max-width: 100%; } + /* Continuity timeline: rotate to a vertical thread down the left */ + .continuity-track { display: none; } + .continuity-events { + grid-template-columns: 1fr; + gap: 20px; + position: relative; + } + .continuity-events::before { + content: ''; + position: absolute; + left: 4px; top: 8px; bottom: 8px; + width: 1px; + background: linear-gradient(180deg, + rgba(154,142,205,0.08) 0%, + rgba(154,142,205,0.35) 55%, + rgba(180,170,235,0.75) 100%); + pointer-events: none; + } + .continuity-event { + grid-template-rows: auto auto; + grid-template-columns: 9px 1fr; + column-gap: 16px; + row-gap: 3px; + } + .continuity-date { grid-column: 2; grid-row: 1; } + .continuity-node { grid-column: 1; grid-row: 1; align-self: center; } + .continuity-text { grid-column: 2; grid-row: 2; max-width: none; } + /* Grids */ - .feature-grid { grid-template-columns: repeat(2, 1fr); } .proof-grid { grid-template-columns: repeat(2, 1fr); } .card-grid.three, .card-grid.four { grid-template-columns: repeat(2, 1fr); } @@ -937,14 +1159,14 @@ @media (max-width: 520px) { h1 { font-size: 38px; } /* hero card naturally sized at 520px */ - /* Feature grid: 2x2 clips the descriptions at phone widths (the cells' - side padding + gaps push two columns past ~390px), so drop to a - single column here — full-width cells wrap cleanly on any phone. */ - .feature-grid { grid-template-columns: 1fr; } .proof-grid { grid-template-columns: 1fr 1fr; } .card-grid.three, .card-grid.four { grid-template-columns: 1fr; } - .hero-actions { flex-direction: column; align-items: stretch; width: 100%; } + /* Primary download goes full-width; alt platforms wrap beneath */ + .download-cta { width: 100%; } + .download-primary { width: 100%; } .btn-primary, .btn-secondary { justify-content: center; } + /* The roster hint crowds the tab row on narrow phones */ + .roster-hint { display: none; } .testimonial-quote { font-size: 16px; } .section-header { margin-bottom: 32px; } .ecosystem-inner { padding: 24px 20px; } @@ -953,12 +1175,12 @@ @media (max-width: 400px) { h1 { font-size: 34px; } - .kicker { font-size: 9px; } + .kicker { font-size: 10px; } /* Uniform 16px gutter at smallest viewports */ .header-inner { padding: 0 16px; } .testimonial-strip { padding: 28px 16px; } footer { padding: 28px 16px; } - .hero { padding: 0 16px; } + .hero { padding: 32px 16px; } .content-section, .proof-section { padding: 40px 16px; } .ecosystem-section { padding: 40px 0; } .ecosystem-inner { padding: 24px 16px; } @@ -975,211 +1197,3 @@ color: var(--coven-violet-light); } - /* ──────────────────────────────────────────────────────────── - Download CTA (Hero) - Predominant platform-aware download block. The .is-primary - link (assigned by JS based on detected platform) gets the - solid violet treatment; the others stay glassy/secondary. - Server-rendered without any .is-primary so the *first* card - (macOS) is the default emphasis fallback if JS never runs. - ──────────────────────────────────────────────────────────── */ - .download-cta { - display: flex; flex-direction: column; - gap: 10px; - margin: 2px 0 0; - max-width: 560px; - } - .download-beta-badge { - display: inline-flex; align-items: center; gap: 8px; - align-self: flex-start; - padding: 4px 10px; - border-radius: 999px; - border: 1px solid rgba(142,61,255,0.35); - background: rgba(142,61,255,0.08); - color: var(--vtext); - font-size: 10.5px; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - } - .beta-dot { - width: 7px; height: 7px; border-radius: 999px; - background: var(--vtext); - box-shadow: 0 0 12px var(--vtext); - animation: download-pulse 2.4s ease-in-out infinite; - } - @keyframes download-pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.55; transform: scale(0.85); } - } - .download-heading { - margin: 0; - font-size: clamp(19px, 2vw, 23px); - font-weight: 700; - letter-spacing: -0.01em; - color: var(--white); - display: flex; flex-wrap: wrap; gap: 6px 10px; - align-items: baseline; - } - .download-heading-pre { color: var(--muted); font-weight: 500; } - .download-heading-platform { color: var(--vtext); } - .download-sub { - margin: 0; - color: var(--muted); - font-size: 13px; - line-height: 1.5; - max-width: 52ch; - } - .download-buttons { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; - margin-top: 2px; - } - .download-btn { - display: flex; align-items: center; gap: 9px; - text-decoration: none; - padding: 10px 12px; - border-radius: 10px; - border: 1px solid var(--border-v); - background: rgba(20, 14, 35, 0.55); - backdrop-filter: blur(6px); - color: var(--white); - transition: background 0.18s, border-color 0.18s, transform 0.18s; - position: relative; - overflow: hidden; - } - .download-btn::after { - content: ""; - position: absolute; inset: 0; - pointer-events: none; - background: linear-gradient(120deg, transparent 35%, rgba(255,255,255,0.05) 50%, transparent 65%); - transform: translateX(-100%); - transition: transform 0.55s ease; - } - .download-btn:hover { - border-color: rgba(142,61,255,0.5); - background: rgba(34, 22, 58, 0.7); - transform: translateY(-1px); - } - .download-btn:hover::after { transform: translateX(100%); } - .download-btn-icon { - flex: 0 0 auto; - width: 19px; height: 19px; - display: flex; align-items: center; justify-content: center; - color: var(--vtext); - } - .download-btn-icon svg { width: 100%; height: 100%; } - .download-btn-text { - display: flex; flex-direction: column; gap: 2px; - flex: 1 1 auto; - min-width: 0; - } - /* Compact 4-across hero download row: show a short platform word - ('macOS', 'Windows', 'Linux', 'iOS') rather than the full 'Download - for …' so four chips fit on one slim line. */ - .download-btn-label { - font-size: 12.5px; font-weight: 700; - letter-spacing: 0.01em; - color: var(--white); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - /* Compact hero: the per-platform sub-line ('.dmg · signed', etc.) is hidden - on the standard buttons to keep the download grid to two slim rows and - the whole hero within one viewport. It stays visible on the detected - (is-primary) button, which gets the full rich treatment below. */ - .download-btn-sub { - font-size: 11px; - font-weight: 500; - color: var(--muted); - letter-spacing: 0.02em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: none; - } - .download-btn-arrow { - flex: 0 0 auto; - color: var(--muted); - font-size: 14px; - transition: color 0.15s, transform 0.18s; - } - .download-btn:hover .download-btn-arrow { - color: var(--vtext); - transform: translateY(1px); - } - .download-btn[data-platform="ios"] { - grid-column: auto; - } - - /* Primary (auto-detected platform) treatment — emphasized IN PLACE (violet - fill) rather than spanning the full row, so the download row stays a - stable single line of four in every state and the hero never grows a - surprise extra row that pushes content below the fold. */ - .download-btn.is-primary { - grid-column: auto; - background: linear-gradient(135deg, var(--violet-cta) 0%, #553f9a 100%); - border-color: transparent; - box-shadow: 0 8px 24px -8px rgba(109, 91, 175, 0.65), - 0 0 0 1px rgba(255,255,255,0.06) inset; - } - .download-btn.is-primary .download-btn-icon { color: #fff; } - .download-btn.is-primary .download-btn-label { color: #fff; } - .download-btn.is-primary .download-btn-sub { color: rgba(255,255,255,0.78); display: none; } - .download-btn.is-primary .download-btn-arrow { - color: #fff; - font-size: 17px; - } - .download-btn.is-primary:hover { - transform: translateY(-1px); - background: linear-gradient(135deg, #7e6cc4 0%, #5e479e 100%); - box-shadow: 0 12px 28px -8px rgba(109, 91, 175, 0.8), - 0 0 0 1px rgba(255,255,255,0.1) inset; - } - - .download-all { - display: inline-block; - margin-top: 10px; - color: var(--muted); - text-decoration: none; - font-size: 12px; - letter-spacing: 0.04em; - transition: color 0.15s; - } - .download-all:hover { color: var(--vtext); } - - /* Secondary hero actions (Discord + Docs) — kept low-key - beneath the primary download block. */ - .hero-actions-secondary { - margin-top: 6px; - gap: 10px; - } - .btn-ghost { - display: inline-flex; align-items: center; gap: 8px; - background: transparent; - color: var(--muted); text-decoration: none; - font-size: 12px; font-weight: 600; - letter-spacing: 0.04em; - padding: 8px 14px; border-radius: 6px; - transition: color 0.15s; - } - .btn-ghost:hover { color: var(--white); } - .btn-ghost svg { width: 14px; height: 11px; fill: currentColor; } - - @media (max-width: 720px) { - .download-buttons { grid-template-columns: 1fr; } - .download-btn.is-primary { grid-column: 1; } - } - @media (max-width: 420px) { - /* Narrow phones: tighten padding so the buttons stay visually - contained inside the (pre-existing) narrow hero gutter. */ - .download-cta { gap: 12px; } - .download-btn { padding: 12px 14px; gap: 10px; } - .download-btn.is-primary { padding: 14px 16px; } - .download-btn-label { font-size: 12.5px; } - .download-btn.is-primary .download-btn-label { font-size: 14px; } - .download-btn-sub { font-size: 10.5px; } - .download-heading { font-size: 18px; } - } From 78d914f5602520782a7aaa1e9e4db3c674a88503 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 6 Jul 2026 23:20:53 -0500 Subject: [PATCH 2/3] Polish hero: cursor tracking, load orchestration, dev-scan fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Terminal cursor hugs the end of the typed command again (flex: 0 1 auto instead of grow, still shrinkable for long commands) - Orchestrated hero load: copy lands, terminal answers (+0.12s), then the continuity thread grows node by node left to right (+0.28-0.70s) - Reword Architecture.astro frontmatter comment — the literal '