diff --git a/astro.config.mjs b/astro.config.mjs
index 66249c3..8681929 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -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'],
diff --git a/src/components/Answers.astro b/src/components/Answers.astro
new file mode 100644
index 0000000..6d1fd7b
--- /dev/null
+++ b/src/components/Answers.astro
@@ -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(/
/g)?.length ?? 0;
+if (items !== icons.length) throw new Error(`: ${items} answers and ${icons.length} icons`);
+let n = 0;
+const drawn = html.replace(//g, () => ` ${iconSVG(icons[n++])} `);
+---
+
+
+
+
diff --git a/src/components/BentoCard.astro b/src/components/BentoCard.astro
index b85ce0f..d46db19 100644
--- a/src/components/BentoCard.astro
+++ b/src/components/BentoCard.astro
@@ -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 {
@@ -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);
@@ -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;
}
@@ -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 {
diff --git a/src/components/Cta.astro b/src/components/Cta.astro
index 2da2c2b..84aaa6a 100644
--- a/src/components/Cta.astro
+++ b/src/components/Cta.astro
@@ -10,7 +10,7 @@ interface Props {
const { title, links } = Astro.props;
---
-
+
{title}
@@ -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;
}
@@ -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 {
@@ -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);
}
diff --git a/src/components/Head.astro b/src/components/Head.astro
index d62852a..32ae8d0 100644
--- a/src/components/Head.astro
+++ b/src/components/Head.astro
@@ -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(/\/$/, '');
---
+{[sans400, sans600, mono400, mono600].map((href) => )}
diff --git a/src/components/Hero.astro b/src/components/Hero.astro
index 7b98f04..ae9847c 100644
--- a/src/components/Hero.astro
+++ b/src/components/Hero.astro
@@ -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);
@@ -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;
@@ -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 {
@@ -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);
- }
- }
}
diff --git a/src/components/Icon.astro b/src/components/Icon.astro
index 2cce070..a259a10 100644
--- a/src/components/Icon.astro
+++ b/src/components/Icon.astro
@@ -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;
---
-
-
-
+
diff --git a/src/components/LayerStack.astro b/src/components/LayerStack.astro
index 90583bd..6479d51 100644
--- a/src/components/LayerStack.astro
+++ b/src/components/LayerStack.astro
@@ -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 {
@@ -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 {
diff --git a/src/components/Stats.astro b/src/components/Stats.astro
index b532ad3..26765e0 100644
--- a/src/components/Stats.astro
+++ b/src/components/Stats.astro
@@ -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);
@@ -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;
diff --git a/src/components/Story.astro b/src/components/Story.astro
index 80d8377..8a81d97 100644
--- a/src/components/Story.astro
+++ b/src/components/Story.astro
@@ -38,6 +38,7 @@ const screen = steps
'',
)
.join('');
+const plain = esc(steps.map((s) => [...s.commands.map((c) => `$ ${c}`), ...stepOutput(s)].join('\n')).join('\n\n'));
---
@@ -61,14 +62,18 @@ const screen = steps
))
}
-
+
-
demo — spin
- Copy commands
+ Skip
+ Replay
+ Copy commands
-
'} />
+ '} />
+
+