Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export default defineConfig({
description:
'Self-hosted sandboxes in your own AWS account: a virtual machine per workspace, snapshots with memory, rollback, and a diff of what changed.',
// The organization's own mark (spin-stack/brand): the tile on its navy in dark, and clear in light,
// where the tile would read as a sticker.
logo: { dark: './src/assets/logo.png', light: './src/assets/logo-light.png', alt: 'Spinbox' },
// where the tile would read as a sticker. No alt: the name is written beside it.
logo: { dark: './src/assets/logo.png', light: './src/assets/logo-light.png', alt: '' },
favicon: '/favicon.png',
social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/spin-stack' }],
customCss: ['./src/styles/brand-tokens.css', './src/styles/brand-starlight.css', './src/styles/custom.css'],
Expand Down
61 changes: 61 additions & 0 deletions src/components/Answers.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
// The straight answers - each a limit row of the status page - as a row of cards, an icon on
// each. The list stays the page's Markdown, so the twin is the same list (src/lib/pages.ts drops
// the tag); an icon per answer, in order, and a build that fails when the two counts differ.
import { iconSVG, type IconName } from '../lib/icons';

interface Props {
icons: IconName[];
}
const { icons } = Astro.props;
const html = await Astro.slots.render('default');
const items = html.match(/<li>/g)?.length ?? 0;
if (items !== icons.length) throw new Error(`<Answers>: ${items} answers and ${icons.length} icons`);
let n = 0;
const drawn = html.replace(/<li>/g, () => `<li><span class="icon">${iconSVG(icons[n++])}</span>`);
---

<div class="answers" set:html={drawn} />

<style>
.answers :global(ul) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
gap: 0.75rem;
padding: 0;
margin: 1.5rem 0 1rem;
list-style: none;
}
.answers :global(li) {
display: flex;
flex-direction: column;
gap: 0.6rem;
margin: 0;
padding: 1.1rem 1.1rem 1.2rem;
border: 1px solid var(--spin-border);
border-radius: var(--spin-radius);
background: var(--spin-surface);
color: var(--spin-muted);
font-size: var(--sl-text-sm);
transition: border-color var(--spin-duration) var(--spin-ease);
}
.answers :global(li:hover) {
border-color: var(--spin-border-strong);
}
.answers :global(li strong) {
display: block;
color: var(--sl-color-white);
font-family: var(--sl-font-mono);
font-size: var(--sl-text-base);
margin-bottom: 0.2rem;
}
.answers :global(.icon) {
display: inline-grid;
place-items: center;
width: 2.5rem;
height: 2.5rem;
border-radius: var(--spin-radius);
color: var(--spin-label);
background: var(--spin-raised);
}
</style>
14 changes: 7 additions & 7 deletions src/components/BentoCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,16 @@ const proof = kind === 'e2e' ? `e2e · ${what}` : kind === 'cli' ? `spin ${what}
flex-direction: column;
gap: 0.6rem;
padding: 1.4rem 1.4rem 1.2rem;
border-radius: 18px;
border-radius: var(--spin-radius-lg);
text-decoration: none;
color: var(--sl-color-gray-3);
background: color-mix(in srgb, var(--sl-color-gray-6) 70%, transparent);
border: 1px solid var(--sl-color-hairline-light);
overflow: hidden;
transition:
transform 300ms cubic-bezier(0.2, 0.7, 0.2, 1),
border-color 300ms,
box-shadow 300ms;
transform var(--spin-duration) var(--spin-ease),
border-color var(--spin-duration) var(--spin-ease),
box-shadow var(--spin-duration) var(--spin-ease);
}
.card:hover,
.card:focus-visible {
Expand All @@ -86,7 +86,7 @@ const proof = kind === 'e2e' ? `e2e · ${what}` : kind === 'cli' ? `spin ${what}
place-items: center;
width: 2.6rem;
height: 2.6rem;
border-radius: 12px;
border-radius: var(--spin-radius);
color: var(--spin-label);
background: linear-gradient(135deg, color-mix(in srgb, var(--spin-brand-cyan) 18%, transparent), color-mix(in srgb, var(--spin-brand-blue) 18%, transparent));
border: 1px solid color-mix(in srgb, var(--spin-brand-cyan) 28%, transparent);
Expand Down Expand Up @@ -122,7 +122,7 @@ const proof = kind === 'e2e' ? `e2e · ${what}` : kind === 'cli' ? `spin ${what}
stroke-width: 1.5;
}
.ring-label {
font-size: 10px;
font-size: 11px;
fill: var(--sl-color-gray-3);
letter-spacing: 0.05em;
}
Expand All @@ -135,7 +135,7 @@ const proof = kind === 'e2e' ? `e2e · ${what}` : kind === 'cli' ? `spin ${what}
}
@media (prefers-reduced-motion: no-preference) {
.r3 {
animation: ring-breathe 3.2s ease-in-out infinite alternate;
animation: ring-breathe 1.6s ease-in-out 2 alternate;
}
@keyframes ring-breathe {
to {
Expand Down
36 changes: 13 additions & 23 deletions src/components/Cta.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Props {
const { title, links } = Astro.props;
---

<section class="cta not-content">
<section class="cta not-content spin-dark">
<div class="mesh" aria-hidden="true"></div>
<h2>{title}</h2>
<p><slot /></p>
Expand All @@ -32,17 +32,17 @@ const { title, links } = Astro.props;
overflow: hidden;
margin-block: 4rem 1rem;
padding: clamp(2.5rem, 2rem + 3vw, 4.5rem) clamp(1.5rem, 1rem + 3vw, 4rem);
border-radius: 24px;
background: #0b1120;
color: #e6edf6;
border-radius: var(--spin-radius-lg);
background: var(--spin-page);
color: var(--spin-text);
text-align: center;
border: 1px solid #2a3a58;
border: 1px solid var(--spin-border);
}
.mesh {
position: absolute;
inset: -40%;
z-index: -1;
background: conic-gradient(from 180deg at 50% 50%, #0c3c84, #2f6be3, #27bfe8, #5fe3d6, #0c3c84);
background: conic-gradient(from 180deg at 50% 50%, var(--spin-brand-deep), var(--spin-brand-blue), var(--spin-brand-cyan), var(--spin-brand-mint), var(--spin-brand-deep));
filter: blur(70px);
opacity: 0.55;
}
Expand All @@ -53,12 +53,12 @@ const { title, links } = Astro.props;
font-size: clamp(1.8rem, 1.2rem + 2.4vw, 3rem);
line-height: 1.1;
letter-spacing: -0.035em;
color: #fff;
color: var(--spin-brand-frost);
}
p {
margin: 1rem auto 2rem;
max-width: 40rem;
color: #c8d3e4;
color: var(--spin-text);
font-size: var(--sl-text-lg);
}
.links {
Expand All @@ -68,22 +68,12 @@ const { title, links } = Astro.props;
gap: 1rem;
}
.links :global(.sl-link-button.secondary) {
color: #fff;
border-color: rgb(255 255 255 / 0.4);
color: var(--spin-brand-frost);
border-color: var(--spin-border-strong);
}
.links :global(.sl-link-button.primary) {
background: #fff;
border-color: #fff;
color: #0b1120;
}
@media (prefers-reduced-motion: no-preference) {
.mesh {
animation: cta-spin 22s linear infinite;
}
@keyframes cta-spin {
to {
transform: rotate(1turn);
}
}
background: var(--spin-brand-frost);
border-color: var(--spin-brand-frost);
color: var(--spin-page);
}
</style>
8 changes: 8 additions & 0 deletions src/components/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@
// does not resolve.
import Default from '@astrojs/starlight/components/Head.astro';
import { twinPath } from '../lib/twin';
// The faces the first screen sets, fetched with the page: found late, through the stylesheet, a
// face swaps in after the first paint and the notice and the hero move with it (a layout shift of
// 0.23 on a phone, measured).
import sans400 from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-400-normal.woff2?url';
import sans600 from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-600-normal.woff2?url';
import mono400 from '@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2?url';
import mono600 from '@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-600-normal.woff2?url';

const base = import.meta.env.BASE_URL.replace(/\/$/, '');
---

<Default />
<link rel="alternate" type="text/markdown" href={`${base}${twinPath(Astro.locals.starlightRoute.id)}`} />
{[sans400, sans600, mono400, mono600].map((href) => <link rel="preload" as="font" type="font/woff2" href={href} crossorigin />)}
21 changes: 8 additions & 13 deletions src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,15 @@ const rest = cut < 0 ? '' : title.slice(cut + 2);
margin: 0;
}
.hl {
background: linear-gradient(100deg, var(--spin-brand-cyan), var(--spin-brand-blue) 55%, var(--spin-brand-mint));
/* The mark's lights that may be text on the navy: cyan and mint, never its blue. */
background: linear-gradient(100deg, var(--spin-brand-cyan), var(--spin-brand-mint) 55%, var(--spin-brand-cyan));
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
:global([data-theme='light']) .hl {
background-image: linear-gradient(100deg, #0a6fa3, var(--spin-brand-blue) 55%, var(--spin-brand-deep));
background-image: linear-gradient(100deg, var(--spin-accent), var(--spin-brand-deep) 55%, var(--spin-accent));
}
.tagline {
font-size: var(--sl-text-lg);
Expand Down Expand Up @@ -138,7 +139,7 @@ const rest = cut < 0 ? '' : title.slice(cut + 2);
}
@media (prefers-reduced-motion: no-preference) {
.copy > * {
animation: hero-rise 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
animation: hero-rise var(--spin-duration-draw) var(--spin-ease) both;
}
.copy > :nth-child(2) {
animation-delay: 80ms;
Expand All @@ -149,14 +150,13 @@ const rest = cut < 0 ? '' : title.slice(cut + 2);
.copy > :nth-child(4) {
animation-delay: 240ms;
}
/* Once each (palette.toml's [motion]): the light crosses the title, the dot pulses three
times, and both stay as they end. */
.hl {
animation: hero-sheen 8s ease-in-out infinite alternate;
animation: hero-sheen 2.4s var(--spin-ease) 400ms both;
}
.dot {
animation: hero-pulse 2.4s ease-out infinite;
}
.backdrop {
animation: hero-drift 18s ease-in-out infinite alternate;
animation: hero-pulse 1.6s ease-out 3;
}
@keyframes hero-rise {
from {
Expand All @@ -175,10 +175,5 @@ const rest = cut < 0 ? '' : title.slice(cut + 2);
box-shadow: 0 0 0 0.6rem transparent;
}
}
@keyframes hero-drift {
to {
transform: translate3d(-2%, 3%, 0) scale(1.04);
}
}
}
</style>
22 changes: 5 additions & 17 deletions src/components/Icon.astro
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
---
// The landing page's few icons, drawn here (24-unit strokes in the current colour) so the site
// loads nothing from anyone else.
const paths = {
machine: 'M4 5h16v10H4zM8 19h8M12 15v4M7 9l2 2-2 2M11 13h4',
memory: 'M6 4h12v16H6zM9 8h6M9 12h6M9 16h3M3 8h3M3 12h3M3 16h3M18 8h3M18 12h3M18 16h3',
diff: 'M7 4v10M7 18v2M4 7h6M17 4v16M14 12h6M12 2v20',
shield: 'M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6zM9 12l2 2 4-4',
key: 'M14 10a4 4 0 1 0-1.2 2.8L20 20M17 17l2-2M15 15l1.5-1.5',
agent: 'M12 3v3M7 7h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3zM9 12h.01M15 12h.01M9 16h6',
cloud: 'M7 18a5 5 0 0 1-.6-10A6 6 0 0 1 18 9a4.5 4.5 0 0 1-.5 9zM12 11v5M10 14l2 2 2-2',
move: 'M4 12h16M16 8l4 4-4 4M8 8l-4 4 4 4',
moon: 'M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z',
} as const;
// One of the landing page's icons (src/lib/icons.ts).
import { iconSVG, type IconName } from '../lib/icons';

interface Props {
name: keyof typeof paths;
name: IconName;
}
const { name } = Astro.props;
---

<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d={paths[name]}></path>
</svg>
<Fragment set:html={iconSVG(name)} />
10 changes: 5 additions & 5 deletions src/components/LayerStack.astro
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const layers = [
machine writes; then what a diff and a restore do with them. */
@media (prefers-reduced-motion: no-preference) {
.layer {
animation: ls-drop 650ms cubic-bezier(0.2, 0.8, 0.25, 1.15) both;
animation: ls-drop var(--spin-duration-draw) var(--spin-ease) both;
animation-delay: calc(150ms + var(--i) * 180ms);
}
.verb .bracket {
Expand All @@ -104,13 +104,13 @@ const layers = [
}
.live {
animation:
ls-write 1.6s linear infinite,
ls-fade 400ms ease-out both;
ls-write 1.6s linear 3,
ls-fade var(--spin-duration-slow) ease-out both;
}
.live-top {
animation:
ls-write 1.6s linear infinite,
ls-glow 2.4s ease-in-out infinite alternate;
ls-write 1.6s linear 3,
ls-glow 2.4s ease-in-out 2 alternate;
}
@keyframes ls-drop {
from {
Expand Down
9 changes: 7 additions & 2 deletions src/components/Stats.astro
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const stats = [
grid-template-columns: repeat(2, 1fr);
gap: 1px;
margin-block: 1.5rem 1rem;
border-radius: 18px;
border-radius: var(--spin-radius-lg);
overflow: hidden;
background: var(--sl-color-hairline-light);
border: 1px solid var(--sl-color-hairline-light);
Expand All @@ -85,11 +85,16 @@ const stats = [
line-height: 1;
letter-spacing: -0.04em;
font-variant-numeric: tabular-nums;
background: linear-gradient(120deg, var(--spin-brand-cyan), var(--spin-brand-blue));
/* Cyan to mint on the navy; on white the accent to the mark's base, which hold AA where the
mark's cyan is 2:1. */
background: linear-gradient(120deg, var(--spin-brand-cyan), var(--spin-brand-mint));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
:global([data-theme='light']) .n {
background-image: linear-gradient(120deg, var(--spin-accent), var(--spin-brand-deep));
}
.label {
color: var(--sl-color-white);
font-weight: 600;
Expand Down
Loading
Loading