diff --git a/README.md b/README.md index 2492c9f..f7fabb2 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,12 @@ npm run preview ## Deployment Production will deploy from the tested `main` branch to the Arca Computer Cloudflare account after the company zone is attached and the first deployment is verified. The Vercel deployment remains the live rollback target until that cutover passes. + +## Motion + +The homepage (`src/pages/index.astro`) and the company deck (`src/pages/deck.astro`, served at `/deck`) share one motion engine: `public/motion.css` plus the scripts below. Each page keeps its own stylesheet. Both pages are fully readable without JavaScript. A one-line inline script in `src/layouts/BaseLayout.astro` adds `html.js`, and every hidden-until-revealed rule in `public/styles.css` is scoped to that class and to `prefers-reduced-motion: no-preference`. + +- `src/scripts/motion.ts` — scroll reveals, condensed masthead + progress line, active nav, pointer highlights, magnetic buttons, metric counters, and the self-drawing signal map. +- `src/scripts/signal-board.ts` — the hero canvas (procedural traces with travelling pulses). It pauses off-screen and when the tab is hidden, and renders one static frame under reduced motion. + +Print styles force every reveal visible and hide the canvas, so the deck still exports cleanly to PDF. To test the reduced-motion path, emulate `prefers-reduced-motion: reduce` in DevTools (Rendering panel) and reload. diff --git a/astro.config.mjs b/astro.config.mjs index e57af5d..05f4638 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,5 +4,6 @@ import sitemap from '@astrojs/sitemap'; export default defineConfig({ site: 'https://arca.computer', output: 'static', - integrations: [sitemap({ customPages: ['https://arca.computer/deck'] })], + trailingSlash: 'never', + integrations: [sitemap()], }); diff --git a/public/deck/deck.js b/public/deck/deck.js deleted file mode 100644 index 4e997d2..0000000 --- a/public/deck/deck.js +++ /dev/null @@ -1,25 +0,0 @@ -(() => { - const sections = [...document.querySelectorAll("main > section[id]")]; - const links = [...document.querySelectorAll(".rail a")]; - - if (!("IntersectionObserver" in window)) return; - - const observer = new IntersectionObserver( - (entries) => { - const visible = entries - .filter((entry) => entry.isIntersecting) - .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; - - if (!visible) return; - - links.forEach((link) => { - const current = link.getAttribute("href") === `#${visible.target.id}`; - if (current) link.setAttribute("aria-current", "true"); - else link.removeAttribute("aria-current"); - }); - }, - { threshold: [0.35, 0.6] } - ); - - sections.forEach((section) => observer.observe(section)); -})(); diff --git a/public/deck/styles.css b/public/deck/styles.css index 9a63d6b..acf1d36 100644 --- a/public/deck/styles.css +++ b/public/deck/styles.css @@ -84,7 +84,7 @@ a:focus-visible, [tabindex="-1"]:focus-visible { outline: 2px solid var(--signal } .rail a:hover, .rail a[aria-current="true"] { color: var(--signal); border-color: var(--line-strong); background: var(--signal-soft); } -main { scroll-snap-type: y proximity; } +main { position: relative; z-index: 1; scroll-snap-type: y proximity; } main:focus { outline: none; } .slide { position: relative; min-height: 100svh; display: grid; align-items: center; @@ -281,14 +281,57 @@ h2 { max-width: 900px; font-size: clamp(48px, 6.7vw, 90px); line-height: .95; } .proof-links { grid-template-columns: 1fr; } } -@media (prefers-reduced-motion: reduce) { - html { scroll-behavior: auto; } - * { transition-duration: .01ms !important; animation-duration: .01ms !important; } -} - @media print { - .deck-header, .rail, .grain, .skip-link { display: none; } + .deck-header, .rail, .grain, .skip-link, .signal-board { display: none; } body { background: var(--ink); } .slide { min-height: 100vh; break-after: page; padding: 52px; } .slide:last-child { break-after: auto; } } + +/* Deck motion: entrance choreography and cover diagram (engine in /motion.css). */ +.deck-header::after { + position: absolute; right: 0; bottom: -1px; left: 0; height: 1px; content: ""; + background: var(--signal); transform: scaleX(var(--progress, 0)); transform-origin: left; +} +.deck-header.is-condensed { background: rgba(13,20,18,.9); } + +h1 .line { display: block; overflow: hidden; padding: .08em 0 .18em; margin: -.08em 0 -.18em; } +h1 .line > span { display: block; } + +.metrics strong { font-variant-numeric: tabular-nums; } +.range-grid a.glow { --glow: var(--bronze-rgb); } +.system-loop li { transition: background .3s var(--ease-out); } +.system-loop li:hover { background: rgba(17,27,24,.7); } +.launchpad-grid article { transition: background .3s var(--ease-out); } +.launchpad-grid article:hover { background: rgba(17,27,24,.75); } + +.cover-diagram span { transition: opacity .6s var(--ease-out), translate .6s var(--ease-out); } +.cover-diagram .signal-packet { fill: #dbfff5; stroke: none; transition: opacity .8s ease 3.4s; } +.cover-diagram[data-draw] path { transition-delay: .9s; } +.cover-diagram.is-drawn circle:nth-of-type(1) { transition-delay: 1s; } +.cover-diagram.is-drawn circle:nth-of-type(2) { transition-delay: 1.4s; } +.cover-diagram.is-drawn circle:nth-of-type(3) { transition-delay: 1.8s; } +.cover-diagram.is-drawn circle:nth-of-type(4) { transition-delay: 2.2s; } +.cover-diagram.is-drawn circle:nth-of-type(5) { transition-delay: 2.6s; } +.cover-diagram.is-drawn circle:nth-of-type(6) { transition-delay: 3s; } +.cover-diagram.is-drawn span:nth-of-type(1) { transition-delay: 1s; } +.cover-diagram.is-drawn span:nth-of-type(2) { transition-delay: 1.4s; } +.cover-diagram.is-drawn span:nth-of-type(3) { transition-delay: 1.8s; } +.cover-diagram.is-drawn span:nth-of-type(4) { transition-delay: 2.2s; } +.cover-diagram.is-drawn span:nth-of-type(5) { transition-delay: 2.6s; } +.cover-diagram.is-drawn span:nth-of-type(6) { transition-delay: 3s; } + +@media (prefers-reduced-motion: no-preference) { + html.js .deck-header { animation: fade-down .8s var(--ease-out) backwards; } + html.js .rail { animation: board-in 1s var(--ease-out) .6s backwards; } + html.js .cover .kicker { animation: fade-up .7s var(--ease-out) .12s backwards; } + html.js .cover h1 .line > span { animation: rise 1s var(--ease-out) .2s backwards; } + html.js .cover h1 .line-2 > span { animation-delay: .3s; } + html.js .cover .lead { animation: fade-up .8s var(--ease-out) .48s backwards; } + html.js .cover-index p { animation: fade-up .8s var(--ease-out) backwards; animation-delay: calc(.6s + var(--i, 0) * 80ms); } + html.js .cover-index p:nth-child(2) { --i: 1; } + html.js .cover-index p:nth-child(3) { --i: 2; } + html.js .cover-index p:nth-child(4) { --i: 3; } + html.js .cover-diagram { animation: fade-up .9s var(--ease-out) .8s backwards; } + html.js .cover-diagram:not(.is-drawn) span { opacity: 0; translate: 0 6px; } +} diff --git a/public/motion.css b/public/motion.css new file mode 100644 index 0000000..7daa6d7 --- /dev/null +++ b/public/motion.css @@ -0,0 +1,247 @@ +/* Shared motion layer for arca.computer pages. Loaded after each page's own stylesheet. */ + +:root { + --signal-rgb: 104, 219, 195; + --bronze-rgb: 192, 139, 84; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + --ease-io: cubic-bezier(0.65, 0, 0.35, 1); + --t-micro: 180ms; + --t-reveal: 800ms; +} + +/* Decorative canvas layer behind the first screen. */ +.signal-board { + position: absolute; + inset: 0 0 auto; + z-index: 0; + height: min(100vh, 920px); + overflow: hidden; + pointer-events: none; + -webkit-mask-image: + linear-gradient(90deg, rgba(0, 0, 0, 0.16) 0%, rgba(0, 0, 0, 0.4) 34%, #000 60%), + linear-gradient(180deg, #000 58%, transparent 100%); + -webkit-mask-composite: source-in; + mask-image: + linear-gradient(90deg, rgba(0, 0, 0, 0.16) 0%, rgba(0, 0, 0, 0.4) 34%, #000 60%), + linear-gradient(180deg, #000 58%, transparent 100%); + mask-composite: intersect; +} + +.signal-board canvas { + display: block; + width: 100%; + height: 100%; +} + +/* Magnetic buttons (class added by motion.ts). */ +.button.is-magnetic { + transition: + border-color var(--t-micro) ease, + background var(--t-micro) ease, + color var(--t-micro) ease, + transform 0.4s var(--ease-out); +} + +.button.is-magnetic.is-returning { + transition-duration: var(--t-micro), var(--t-micro), var(--t-micro), 0.6s; + transition-timing-function: ease, ease, ease, var(--ease-spring); +} + +/* Self-drawing diagrams: any [data-draw] container with SVG paths and circles. */ +[data-draw] path { + transition: stroke-dashoffset 2.2s var(--ease-io); +} + +[data-draw] circle { + transition: opacity 0.5s var(--ease-out); +} + +/* Pointer-follow highlight and border glow on .glow cards. */ +@media (hover: hover) and (pointer: fine) { + .glow { + position: relative; + isolation: isolate; + } + + .glow::before { + position: absolute; + inset: 0; + z-index: -1; + content: ""; + pointer-events: none; + opacity: 0; + background: radial-gradient( + 380px circle at var(--mx, 50%) var(--my, 50%), + rgba(var(--glow, var(--signal-rgb)), 0.11), + transparent 62% + ); + transition: opacity 0.4s var(--ease-out); + } + + .glow::after { + position: absolute; + inset: -1px; + z-index: 2; + padding: 1px; + content: ""; + pointer-events: none; + opacity: 0; + background: radial-gradient( + 320px circle at var(--mx, 50%) var(--my, 50%), + rgba(var(--glow, var(--signal-rgb)), 0.8), + transparent 60% + ); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask-composite: exclude; + transition: opacity 0.4s var(--ease-out); + } + + .glow:hover::before, + .glow:hover::after { + opacity: 1; + } +} + +@media (max-width: 760px) { + .signal-board { + height: min(100vh, 760px); + opacity: 0.6; + -webkit-mask-image: linear-gradient(180deg, #000 55%, transparent 100%); + -webkit-mask-composite: source-over; + mask-image: linear-gradient(180deg, #000 55%, transparent 100%); + mask-composite: add; + } +} + +@keyframes pulse { + 50% { + opacity: 0.42; + filter: drop-shadow(0 0 2px var(--signal)); + } +} + +@keyframes fade-down { + from { + opacity: 0; + transform: translateY(-12px); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes rise { + from { + transform: translateY(112%); + } + to { + transform: none; + } +} + +@keyframes board-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@media (prefers-reduced-motion: no-preference) { + html.js .signal-board { + animation: board-in 2s var(--ease-out) 0.35s backwards; + } + + /* Scroll reveals: hidden only when the motion script can run. */ + html.js [data-reveal]:not(.is-done), + html.js [data-reveal-stagger] > :not(.is-done) { + opacity: 0; + transform: translateY(26px); + filter: blur(5px); + transition: + opacity var(--t-reveal) var(--ease-out), + transform var(--t-reveal) var(--ease-out), + filter var(--t-reveal) var(--ease-out); + transition-delay: calc(var(--i, 0) * 70ms); + } + + html.js [data-reveal].is-in:not(.is-done), + html.js [data-reveal-stagger] > .is-in:not(.is-done) { + opacity: 1; + transform: none; + filter: none; + } + + /* Diagram nodes stay hidden until the line has drawn itself. */ + html.js [data-draw]:not(.is-drawn) circle { + opacity: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + .signal-packet { + display: none; + } + + .signal-board { + opacity: 0.7; + } + + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.15s !important; + transition-delay: 0s !important; + animation-duration: 0.01ms !important; + animation-delay: 0s !important; + animation-iteration-count: 1 !important; + } +} + +@media print { + .signal-board, + .signal-packet { + display: none; + } + + html.js [data-reveal], + html.js [data-reveal-stagger] > *, + html.js [data-draw] circle { + opacity: 1 !important; + transform: none !important; + filter: none !important; + } + + [data-draw] path { + stroke-dasharray: none !important; + stroke-dashoffset: 0 !important; + } + + *, + *::before, + *::after { + animation: none !important; + transition: none !important; + } +} diff --git a/public/styles.css b/public/styles.css index bf1597e..99b7f4c 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,5 +1,3 @@ -@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Manrope:wght@400;500;600&display=swap"); - :root { color-scheme: dark; --ink: #0d1412; @@ -30,6 +28,7 @@ html { } body { + position: relative; margin: 0; min-width: 320px; min-height: 100vh; @@ -95,6 +94,13 @@ footer { margin-inline: auto; } +main { + position: relative; + z-index: 1; + overflow-x: clip; + overflow-clip-margin: 12px; +} + .masthead { position: sticky; top: 0; @@ -109,6 +115,32 @@ footer { font: 400 11px/1 var(--mono); letter-spacing: 0.09em; text-transform: uppercase; + transition: + top 0.45s var(--ease-out), + padding-top 0.45s var(--ease-out), + background 0.45s var(--ease-out); +} + +.masthead::after { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 1px; + content: ""; + background: var(--signal); + transform: scaleX(var(--progress, 0)); + transform-origin: left; +} + +@media (min-width: 761px) { + .masthead.is-condensed { + top: -28px; + padding-top: 28px; + background: rgba(13, 20, 18, 0.8); + -webkit-backdrop-filter: blur(14px) saturate(1.3); + backdrop-filter: blur(14px) saturate(1.3); + } } .wordmark { @@ -127,6 +159,12 @@ footer { height: 32px; border: 1px solid var(--bronze); color: var(--paper); + transition: border-color 0.3s var(--ease-out), background 0.3s var(--ease-out); +} + +.wordmark:hover .mark { + border-color: var(--signal); + background: var(--signal-soft); } .mark svg { @@ -154,15 +192,36 @@ footer { } .masthead nav a { + position: relative; + padding: 6px 0; color: var(--muted); text-decoration: none; - transition: color 140ms ease; + transition: color var(--t-micro) ease; +} + +.masthead nav a::after { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 1px; + content: ""; + background: var(--signal); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.3s var(--ease-out); } -.masthead nav a:hover { +.masthead nav a:hover, +.masthead nav a[aria-current="true"] { color: var(--paper-strong); } +.masthead nav a:hover::after, +.masthead nav a[aria-current="true"]::after { + transform: scaleX(1); +} + .status { display: flex; align-items: center; @@ -184,6 +243,7 @@ main:focus { } .hero { + position: relative; min-height: calc(100vh - 92px); display: flex; flex-direction: column; @@ -229,6 +289,30 @@ h1 { letter-spacing: -0.075em; } +h1 .line { + display: block; + overflow: hidden; + padding: 0.08em 0 0.16em; + margin: -0.08em 0 -0.16em; +} + +h1 .line > span { + display: block; +} + +h1 .line-2 > span { + background: linear-gradient(90deg, var(--paper-strong) 48%, #c2efe0 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; +} + +h1 .line-2 > span::selection { + color: var(--ink-deep); + -webkit-text-fill-color: var(--ink-deep); +} + .lede { margin: 0; padding-bottom: 8px; @@ -257,7 +341,19 @@ h1 { letter-spacing: 0.05em; text-decoration: none; text-transform: uppercase; - transition: border-color 140ms ease, background 140ms ease, color 140ms ease; + transition: + border-color var(--t-micro) ease, + background var(--t-micro) ease, + color var(--t-micro) ease; +} + +.button span { + display: inline-block; + transition: transform 0.25s var(--ease-out); +} + +.button:hover span { + transform: translate(2px, -2px); } .button-primary { @@ -293,6 +389,11 @@ h1 { gap: 18px; padding: 22px 20px; border-left: 1px solid var(--line); + transition: background 0.3s var(--ease-out); +} + +.hero-index div:hover { + background: rgba(13, 20, 18, 0.5); } .hero-index div:last-child { @@ -363,6 +464,11 @@ h1 { min-height: 278px; padding: 32px 30px; border-left: 1px solid var(--line); + transition: background 0.35s var(--ease-out); +} + +.principles article:hover { + background: rgba(13, 20, 18, 0.5); } .principles article:last-child { @@ -376,6 +482,23 @@ h1 { font: 400 10px/1 var(--mono); } +.principles span::after { + display: block; + width: 26px; + height: 1px; + margin-top: 12px; + content: ""; + background: var(--bronze); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.9s var(--ease-out) 0.35s; +} + +.principles article.is-in span::after, +html:not(.js) .principles span::after { + transform: none; +} + .principles h3 { margin: 78px 0 14px; color: var(--paper-strong); @@ -392,12 +515,13 @@ h1 { .work-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(6, minmax(0, 1fr)); border-top: 1px solid var(--line); border-left: 1px solid var(--line); } .work-card { + grid-column: span 2; min-height: 390px; display: flex; flex-direction: column; @@ -405,16 +529,15 @@ h1 { border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: rgba(13, 20, 18, 0.42); + transition: background 0.35s var(--ease-out); } -@media (min-width: 761px) { - .work-card:last-child:nth-child(odd) { - grid-column: 1 / -1; - min-height: 320px; - } +.work-card:hover { + background: rgba(17, 27, 24, 0.72); } .work-card-featured { + grid-column: span 3; min-height: 480px; border-top: 2px solid var(--bronze); background: @@ -422,6 +545,12 @@ h1 { rgba(13, 20, 18, 0.58); } +.work-card-featured:hover { + background: + linear-gradient(135deg, var(--bronze-soft), transparent 46%), + rgba(17, 27, 24, 0.78); +} + .card-topline { display: flex; justify-content: space-between; @@ -436,6 +565,32 @@ h1 { text-align: right; } +.status-pill { + display: inline-flex; + align-items: center; + gap: 7px; + white-space: nowrap; +} + +.status-pill::before { + width: 5px; + height: 5px; + flex: none; + border-radius: 999px; + content: ""; + background: var(--paper); +} + +.status-pill[data-state="live"]::before { + background: var(--signal); + box-shadow: 0 0 10px var(--signal); +} + +.status-pill[data-state="beta"]::before { + background: var(--bronze); + box-shadow: 0 0 8px rgba(192, 139, 84, 0.6); +} + .work-mark { width: 70px; height: 70px; @@ -450,6 +605,13 @@ h1 { background-size: 14px 14px; font: 400 17px/1 var(--mono); letter-spacing: 0.04em; + transition: border-color 0.4s var(--ease-out), background-position 0.9s var(--ease-out), box-shadow 0.4s var(--ease-out); +} + +.work-card:hover .work-mark { + border-color: rgba(104, 219, 195, 0.6); + background-position: 7px 7px, 7px 7px; + box-shadow: 0 0 0 1px rgba(104, 219, 195, 0.12), 0 0 28px rgba(104, 219, 195, 0.12); } .work-card h3 { @@ -484,21 +646,57 @@ h1 { font: 400 9px/1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; + transition: border-color 0.3s var(--ease-out), color 0.3s var(--ease-out); +} + +.work-card:hover .tag-list li { + border-color: var(--line-strong); + color: var(--paper); +} + +.card-link, +.network-note a, +.founder-links a { + text-decoration: none; + background: + linear-gradient(var(--signal), var(--signal)) no-repeat 0 100% / 0 1px, + linear-gradient(var(--bronze), var(--bronze)) no-repeat 0 100% / 100% 1px; + transition: background-size 0.45s var(--ease-out), color var(--t-micro) ease; +} + +.card-link:hover, +.network-note a:hover, +.founder-links a:hover { + background-size: 100% 1px, 100% 1px; } .card-link { width: fit-content; margin-top: 32px; + padding-bottom: 4px; color: var(--paper); font: 400 11px/1.4 var(--mono); letter-spacing: 0.04em; - text-decoration-color: var(--bronze); text-transform: uppercase; } .card-link:hover { color: var(--signal); - text-decoration-color: var(--signal); +} + +.card-link span, +.network-note a span, +.founder-links a span, +.proof-grid i { + display: inline-block; + transition: transform 0.3s var(--ease-out); +} + +.card-link:hover span, +.network-note a:hover span, +.founder-links a:hover span, +.proof-grid a:hover i { + transform: translate(3px, -3px); } .signal-map { @@ -556,6 +754,18 @@ h1 { stroke: var(--signal); } +.signal-map .signal-packet { + fill: #dbfff5; + stroke: none; + transition: opacity 0.8s ease 2.3s; +} + +.signal-map.is-drawn circle:nth-of-type(1) { transition-delay: 0.25s; } +.signal-map.is-drawn circle:nth-of-type(2) { transition-delay: 0.7s; } +.signal-map.is-drawn circle:nth-of-type(3) { transition-delay: 1.15s; } +.signal-map.is-drawn circle:nth-of-type(4) { transition-delay: 1.6s; } +.signal-map.is-drawn circle:nth-of-type(5) { transition-delay: 2.05s; } + .signal-copy { position: absolute; z-index: 2; @@ -566,8 +776,13 @@ h1 { background: var(--ink); font-family: var(--mono); text-transform: uppercase; + transition: opacity 0.7s var(--ease-out), translate 0.7s var(--ease-out); } +.signal-map.is-drawn .signal-copy-start { transition-delay: 0.15s; } +.signal-map.is-drawn .signal-copy-mid { transition-delay: 1.1s; } +.signal-map.is-drawn .signal-copy-end { transition-delay: 2s; } + .signal-copy span { color: var(--bronze); font-size: 9px; @@ -599,6 +814,7 @@ h1 { } .process-grid { + position: relative; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; @@ -607,10 +823,33 @@ h1 { list-style: none; } +.process-grid::before { + position: absolute; + top: -1px; + right: 0; + left: 0; + height: 1px; + content: ""; + background: linear-gradient(90deg, var(--signal), var(--bronze)); + transform: scaleX(0); + transform-origin: left; + transition: transform 1.6s var(--ease-io) 0.2s; +} + +.process-grid.is-in::before, +html:not(.js) .process-grid::before { + transform: none; +} + .process-grid li { min-height: 230px; padding: 30px 24px; border-left: 1px solid var(--line); + transition: background 0.35s var(--ease-out); +} + +.process-grid li:hover { + background: rgba(13, 20, 18, 0.5); } .process-grid li:last-child { @@ -644,16 +883,65 @@ h1 { border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: rgba(13, 20, 18, 0.38); + transition: background 0.35s var(--ease-out); +} + +.agent-grid article:hover { + background: rgba(17, 27, 24, 0.7); +} + +.agent-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; } .agent-code { + display: inline-flex; + align-items: center; + gap: 8px; color: var(--bronze); font: 400 9px/1 var(--mono); letter-spacing: 0.08em; } +.agent-code::before { + width: 5px; + height: 5px; + flex: none; + border-radius: 999px; + content: ""; + background: var(--signal); + box-shadow: 0 0 8px var(--signal); +} + +.sigil { + width: 28px; + height: 28px; + flex: none; + color: var(--paper); + overflow: visible; +} + +.sigil path { + fill: none; + stroke: currentColor; + stroke-width: 1; + stroke-linecap: round; + stroke-linejoin: round; + opacity: 0.7; + stroke-dasharray: 110; + stroke-dashoffset: 0; +} + +.sigil circle { + fill: var(--signal); + transition: r 0.3s var(--ease-out); +} + .agent-grid h3 { - margin: 82px 0 7px; + margin: 74px 0 7px; color: var(--paper-strong); font-size: 27px; font-weight: 500; @@ -690,9 +978,61 @@ h1 { } .network-note a { + padding-bottom: 3px; color: var(--paper); } +.network-note a:hover { + color: var(--signal); +} + +.metrics-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border-top: 1px solid var(--line-strong); + border-left: 1px solid var(--line); +} + +.metrics-strip > a { + min-height: 196px; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 22px; + padding: 26px 24px; + border-right: 1px solid var(--line); + border-bottom: 1px solid var(--line); + background: rgba(13, 20, 18, 0.42); + text-decoration: none; + transition: background 0.35s var(--ease-out); +} + +.metrics-strip > a:hover { + background: rgba(17, 27, 24, 0.72); +} + +.metrics-strip strong { + color: var(--signal); + font: 400 clamp(44px, 5vw, 72px)/1 var(--mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.06em; +} + +.metrics-strip span { + max-width: 260px; + color: var(--muted); + font-size: 12px; + line-height: 1.6; +} + +.as-of { + margin: 18px 0 44px; + color: var(--muted); + font: 400 10px/1.6 var(--mono); + letter-spacing: 0.06em; + text-transform: uppercase; +} + .proof-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -711,7 +1051,7 @@ h1 { border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); text-decoration: none; - transition: background 140ms ease; + transition: background 0.35s var(--ease-out); } .proof-grid a:hover { @@ -759,6 +1099,11 @@ h1 { padding: 17px 24px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); + transition: background 0.3s var(--ease-out); +} + +.company-ledger div:hover { + background: rgba(13, 20, 18, 0.5); } .company-ledger dt, @@ -779,6 +1124,8 @@ h1 { } .founder-card { + --glow: var(--bronze-rgb); + position: relative; min-height: 432px; display: flex; flex-direction: column; @@ -788,7 +1135,20 @@ h1 { background: var(--bronze-soft); } +.monogram { + position: absolute; + top: 14px; + right: 22px; + color: transparent; + font: 500 clamp(96px, 11vw, 150px)/0.8 var(--sans); + letter-spacing: -0.08em; + -webkit-text-stroke: 1px rgba(192, 139, 84, 0.3); + pointer-events: none; + user-select: none; +} + .founder-card h3 { + position: relative; margin: auto 0 20px; color: var(--paper-strong); font-size: clamp(31px, 4vw, 48px); @@ -798,6 +1158,7 @@ h1 { } .founder-card > p:not(.section-index) { + position: relative; margin: 0; color: var(--muted); font-size: 14px; @@ -805,16 +1166,28 @@ h1 { } .founder-links { + position: relative; display: flex; flex-wrap: wrap; - gap: 18px; + gap: 10px; margin-top: 28px; font: 400 10px/1.4 var(--mono); text-transform: uppercase; } .founder-links a { + padding: 9px 12px; + border: 1px solid var(--line); color: var(--paper); + background-size: 0 0, 0 0; + transition: border-color 0.3s var(--ease-out), color 0.3s var(--ease-out), background 0.3s var(--ease-out); +} + +.founder-links a:hover { + color: var(--paper-strong); + border-color: var(--signal); + background-color: var(--signal-soft); + background-size: 0 0, 0 0; } .contact-section { @@ -829,27 +1202,63 @@ h1 { max-width: 830px; } -.contact-link { +.contact-actions { grid-column: 2; - width: fit-content; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 26px; margin-top: 36px; +} + +.contact-link { + width: fit-content; + padding-bottom: 6px; color: var(--signal); font: 400 clamp(22px, 3.4vw, 42px)/1.2 var(--mono); letter-spacing: -0.04em; - text-decoration-color: var(--bronze); + text-decoration: none; + background: + linear-gradient(var(--signal), var(--signal)) no-repeat 0 100% / 0 2px, + linear-gradient(var(--bronze), var(--bronze)) no-repeat 0 100% / 100% 1px; + transition: background-size 0.5s var(--ease-out); +} + +.contact-link:hover { + background-size: 100% 2px, 100% 1px; } footer { + position: relative; min-height: 180px; display: grid; grid-template-columns: 1fr minmax(280px, 0.85fr) auto auto; align-items: center; gap: 34px; + overflow: hidden; border-top: 1px solid var(--line); color: var(--muted); font: 400 10px/1.65 var(--mono); } +.footer-mark { + position: absolute; + right: -0.04em; + bottom: -0.32em; + z-index: 0; + color: transparent; + font: 600 clamp(150px, 22vw, 300px)/1 var(--sans); + letter-spacing: -0.06em; + -webkit-text-stroke: 1px rgba(226, 229, 218, 0.08); + pointer-events: none; + user-select: none; +} + +footer > :not(.footer-mark) { + position: relative; + z-index: 1; +} + footer div { display: flex; flex-direction: column; @@ -869,10 +1278,30 @@ footer nav { gap: 16px; } +footer nav a { + text-decoration: none; + background: linear-gradient(var(--signal), var(--signal)) no-repeat 0 100% / 0 1px; + transition: background-size 0.35s var(--ease-out), color var(--t-micro) ease; +} + +footer nav a:hover { + background-size: 100% 1px; +} + footer a:hover { color: var(--signal); } +@media (hover: hover) and (pointer: fine) { + .agent-grid article:hover .sigil path { + animation: sigil-draw 1s var(--ease-out); + } + + .agent-grid article:hover .sigil circle { + r: 2.4; + } +} + @media (max-width: 1080px) { .masthead { grid-template-columns: auto 1fr; @@ -913,6 +1342,10 @@ footer a:hover { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .metrics-strip { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .company-grid { grid-template-columns: 1fr; } @@ -946,6 +1379,10 @@ footer a:hover { padding: 18px 0 14px; } + .masthead::after { + display: none; + } + .masthead nav { width: 100%; justify-content: flex-start; @@ -964,6 +1401,7 @@ footer a:hover { min-height: 32px; display: inline-flex; align-items: center; + padding: 0; } .hero { @@ -1050,6 +1488,7 @@ footer a:hover { .work-card, .work-card-featured { + grid-column: auto; min-height: 0; padding: 26px; } @@ -1059,10 +1498,6 @@ footer a:hover { } .work-card h3 { - margin: 64px 0 14px; - } - - .work-card-featured h3 { margin-top: 0; } @@ -1102,7 +1537,7 @@ footer a:hover { } .agent-grid h3 { - margin-top: 44px; + margin-top: 40px; } .network-note { @@ -1110,6 +1545,15 @@ footer a:hover { flex-direction: column; } + .metrics-strip > a { + min-height: 160px; + padding: 22px 18px; + } + + .as-of { + margin-bottom: 36px; + } + .proof-grid a { min-height: 170px; padding: 24px; @@ -1125,13 +1569,23 @@ footer a:hover { padding: 26px; } + .monogram { + top: 10px; + right: 14px; + font-size: 96px; + } + .contact-section { padding: 88px 0 94px; } - .contact-link { + .contact-actions { grid-column: 1; margin-top: 12px; + gap: 22px; + } + + .contact-link { font-size: clamp(18px, 5.6vw, 26px); white-space: nowrap; } @@ -1142,6 +1596,12 @@ footer a:hover { padding: 38px 0; } + .footer-mark { + right: -0.03em; + bottom: -0.3em; + font-size: 150px; + } + footer nav a { min-height: 32px; display: inline-flex; @@ -1182,40 +1642,82 @@ footer a:hover { display: none; } + .metrics-strip { + grid-template-columns: 1fr; + } + .proof-grid a { grid-template-columns: 42px 1fr auto; } } +/* Homepage motion: entrance choreography and ambient animation (engine in /motion.css). */ +@keyframes sigil-draw { + from { + stroke-dashoffset: 110; + } + to { + stroke-dashoffset: 0; + } +} + @media (prefers-reduced-motion: no-preference) { .status i, - .signal-active { + .signal-active, + .status-pill[data-state="live"]::before, + .agent-code::before { animation: pulse 2.8s ease-in-out infinite; } - @keyframes pulse { - 50% { - opacity: 0.42; - filter: drop-shadow(0 0 2px var(--signal)); - } + .agent-grid article:nth-child(2) .agent-code::before { animation-delay: -0.6s; } + .agent-grid article:nth-child(3) .agent-code::before { animation-delay: -1.2s; } + .agent-grid article:nth-child(4) .agent-code::before { animation-delay: -1.8s; } + .agent-grid article:nth-child(5) .agent-code::before { animation-delay: -2.4s; } + + /* Entrance choreography (only when the motion script can run). */ + html.js .masthead { + animation: fade-down 0.8s var(--ease-out) backwards; } -} -@media (prefers-reduced-motion: reduce) { - html { - scroll-behavior: auto; + html.js .hero .eyebrow { + animation: fade-up 0.7s var(--ease-out) 0.12s backwards; } - *, - *::before, - *::after { - scroll-behavior: auto !important; - transition-duration: 0.01ms !important; - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; + html.js .hero h1 .line > span { + animation: rise 1s var(--ease-out) 0.2s backwards; + } + + html.js .hero h1 .line-2 > span { + animation-delay: 0.3s; + } + + html.js .lede { + animation: fade-up 0.8s var(--ease-out) 0.48s backwards; + } + + html.js .hero-actions .button { + animation: fade-up 0.7s var(--ease-out) 0.6s backwards; } -} + html.js .hero-actions .button + .button { + animation-delay: 0.68s; + } + + html.js .hero-index > div { + animation: fade-up 0.8s var(--ease-out) backwards; + animation-delay: calc(0.75s + var(--i, 0) * 80ms); + } + + html.js .hero-index > div:nth-child(2) { --i: 1; } + html.js .hero-index > div:nth-child(3) { --i: 2; } + html.js .hero-index > div:nth-child(4) { --i: 3; } + + /* Signal map labels follow the drawing. */ + html.js .signal-map:not(.is-drawn) .signal-copy { + opacity: 0; + translate: 0 8px; + } +} .error-page { min-height: 100vh; diff --git a/scripts/check-build.mjs b/scripts/check-build.mjs index e612cb3..85465cf 100644 --- a/scripts/check-build.mjs +++ b/scripts/check-build.mjs @@ -8,6 +8,8 @@ await Promise.all([ exists('index.html'), exists('404.html'), exists('styles.css'), + exists('motion.css'), + exists('deck/styles.css'), exists('robots.txt'), exists('sitemap-index.xml'), exists('og-image.png'), diff --git a/scripts/qa.mjs b/scripts/qa.mjs index 224c146..e438642 100644 --- a/scripts/qa.mjs +++ b/scripts/qa.mjs @@ -8,6 +8,7 @@ const systemChrome = '/usr/bin/google-chrome'; const browser = await chromium.launch({ executablePath: existsSync(systemChrome) ? systemChrome : undefined, headless: true }); const results = {}; +for (const path of ['/', '/deck']) { for (const [name, viewport] of Object.entries({ desktop: { width: 1440, height: 1000 }, mobile: { width: 390, height: 844 }, @@ -19,8 +20,20 @@ for (const [name, viewport] of Object.entries({ if (message.type() === 'error') consoleErrors.push(message.text()); }); page.on('pageerror', (error) => consoleErrors.push(error.message)); - const response = await page.goto(baseURL, { waitUntil: 'networkidle' }); - await page.screenshot({ path: `artifacts/${name}.png`, fullPage: true }); + const label = path === '/' ? name : `${name}-${path.slice(1)}`; + const response = await page.goto(new URL(path, baseURL).href, { waitUntil: 'networkidle' }); + // Walk the page so scroll-triggered reveals have fired before screenshots and axe run. + await page.evaluate(async () => { + const step = Math.max(240, Math.round(window.innerHeight * 0.6)); + const height = document.documentElement.scrollHeight; + for (let y = 0; y <= height; y += step) { + window.scrollTo({ top: y, behavior: 'instant' }); + await new Promise((resolve) => setTimeout(resolve, 120)); + } + window.scrollTo({ top: 0, behavior: 'instant' }); + }); + await page.waitForTimeout(1500); + await page.screenshot({ path: `artifacts/${label}.png`, fullPage: true }); await page.addScriptTag({ content: AxeBuilder.source }); const axe = await page.evaluate(async () => globalThis.axe.run(document)); const metrics = await page.evaluate(() => { @@ -36,7 +49,7 @@ for (const [name, viewport] of Object.entries({ contactLines: contact ? new Set([...contactRange.getClientRects()].map((rect) => Math.round(rect.top))).size : 0, }; }); - results[name] = { + results[label] = { status: response?.status(), consoleErrors, metrics, @@ -45,6 +58,7 @@ for (const [name, viewport] of Object.entries({ await page.close(); await context.close(); } +} await browser.close(); await writeFile('artifacts/qa.json', JSON.stringify(results, null, 2) + '\n'); diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 4da1b3c..0d8bd47 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -5,6 +5,7 @@ export interface Props { canonicalPath?: string | null; noindex?: boolean; structuredData?: boolean; + stylesheet?: string; } const { @@ -13,6 +14,7 @@ const { canonicalPath = '/', noindex = false, structuredData = true, + stylesheet = '/styles.css', } = Astro.props; const canonicalURL = canonicalPath === null ? null : new URL(canonicalPath, Astro.site).href; --- @@ -50,7 +52,15 @@ const canonicalURL = canonicalPath === null ? null : new URL(canonicalPath, Astr