diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx
index a480924..78304d1 100644
--- a/app/blog/[slug]/page.tsx
+++ b/app/blog/[slug]/page.tsx
@@ -111,7 +111,7 @@ export default async function BlogPost({
← field notes
@@ -121,7 +121,7 @@ export default async function BlogPost({
notes" back-link above stays flush-left as nav chrome. */}
§ {num}·
@@ -168,10 +168,10 @@ export default async function BlogPost({
{/* The title holds steady on hover — the glyph slides +
greens and the label lifts, mirroring the index row's
`→`. */}
-
+
←
{" "}
@@ -189,11 +189,11 @@ export default async function BlogPost({
href={`/blog/${newer.slug}/`}
className="group flex flex-col gap-2 no-underline @xl:items-end @xl:text-right"
>
-
+
later{" "}
→
diff --git a/app/blog/page.tsx b/app/blog/page.tsx
index 72c6240..60e6d85 100644
--- a/app/blog/page.tsx
+++ b/app/blog/page.tsx
@@ -92,7 +92,7 @@ export default function BlogIndex() {
#date
diff --git a/app/globals.css b/app/globals.css
index d65fe44..11e6d14 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -4,6 +4,15 @@
--ink: #000000;
--paper: #ffffff;
--whisper: #0f9d6a;
+ /* Raw --whisper is ~3.5:1 on paper (short of AA for text) and ~6:1 on
+ ink. --whisper-deep mixes it 68% with ink for ~6.5:1 on paper.
+ --whisper-text is what readable brand text uses (highlightBrand, via
+ `text-whisper-text`): raw by default, matching the body's ink ground,
+ and swapped to --whisper-deep by every Frame tone="paper". Decorative
+ glyphs and icons (prompt and arrow marks, the pin, the hero's full stop)
+ keep raw --whisper — ~3.5:1 clears the 3:1 non-text bar. */
+ --whisper-deep: color-mix(in srgb, var(--whisper) 68%, var(--ink));
+ --whisper-text: var(--whisper);
/* Intentionally 0: anchor jumps land flush at viewport top (under
the fixed nav). Section headers don't get clipped because every
Frame's --spacing-frame-pad-y floor (5rem = 80px) sits comfortably
@@ -25,6 +34,7 @@
--color-ink: var(--ink);
--color-paper: var(--paper);
--color-whisper: var(--whisper);
+ --color-whisper-text: var(--whisper-text);
--font-sans:
var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system,
@@ -204,7 +214,6 @@ body {
opacity 800ms var(--ease-soft),
transform 800ms var(--ease-soft);
transition-delay: var(--reveal-delay, 0ms);
- will-change: opacity, transform;
}
[data-reveal][data-revealed="true"] {
@@ -320,18 +329,18 @@ a.cta-primary:focus-visible {
outline-offset: 9px;
}
-/* The status link under the hero terminal. Raw --whisper is ~3.5:1 on
- paper, short of AA at body size, so the value text uses whisper mixed
- 68% with ink (~6.6:1). The label is ink like the stat labels; the dot
- and the arrow keep the full accent. */
+/* The status link under the hero terminal. The value text takes
+ --whisper-deep (see :root) for AA on paper — directly rather than via
+ --whisper-text, so the link holds AA even outside a paper Frame. The label
+ is ink like the stat labels; the dot and the arrow keep the full accent. */
a.hero-status {
- color: color-mix(in srgb, var(--whisper) 68%, var(--ink));
+ color: var(--whisper-deep);
}
.hero-status-text {
padding-bottom: 1px;
border-bottom: 1px solid transparent;
- transition: border-bottom-color 260ms cubic-bezier(0.2, 0.8, 0.2, 1);
+ transition: border-bottom-color 260ms var(--ease-soft);
}
a.hero-status:hover .hero-status-text {
@@ -340,7 +349,7 @@ a.hero-status:hover .hero-status-text {
a.hero-status .arrow {
margin-right: 0;
- transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
+ transition: transform 260ms var(--ease-expo);
}
a.hero-status:hover .arrow {
@@ -370,8 +379,11 @@ a.hero-status:hover .arrow {
Replays recent on-chain settlements from the live stats file
(lib/stats.ts), one per cycle: tx → block → operator → bytes → paid.
All lines share one 4.5s loop; each line fades in at its own delay.
- Progress bar fills during the "delivered" line. Cursor blinks
- independently. The cascade restarts every cycle. Shares the "tool
+ Progress bar fills during the "delivered" line. HeroTerminal advances to
+ the next settlement on .tl-0's animationiteration, so anything that stops
+ the cascade stops the rotation: reduced motion and the offscreen pause
+ below. The cursor blinks independently except offscreen.
+ The cascade restarts every cycle. Shares the "tool
panel" vocabulary with .fleet and .faq-card: soft corners, a solid
1px rule, a hard offset shadow. */
@@ -485,7 +497,7 @@ a.hero-status:hover .arrow {
}
.terminal .dim {
- opacity: 0.45;
+ opacity: 0.6;
}
.terminal .amount {
@@ -506,8 +518,9 @@ a.hero-status:hover .arrow {
.terminal .progress-fill {
position: absolute;
inset: 0;
- width: 0%;
background: var(--whisper);
+ transform: scaleX(0);
+ transform-origin: left center;
animation: term-progress 4.5s linear infinite;
animation-delay: 1.1s;
}
@@ -515,20 +528,20 @@ a.hero-status:hover .arrow {
@keyframes term-progress {
0%,
24% {
- width: 0%;
+ transform: scaleX(0);
}
44% {
- width: 100%;
+ transform: scaleX(1);
}
80% {
- width: 100%;
+ transform: scaleX(1);
}
88% {
- width: 100%;
+ transform: scaleX(1);
opacity: 0;
}
100% {
- width: 0%;
+ transform: scaleX(0);
opacity: 0;
}
}
@@ -560,10 +573,15 @@ a.hero-status:hover .arrow {
transform: none;
}
.terminal .progress-fill {
- width: 100%;
+ transform: none;
}
}
+/* Set by HeroTerminal while the panel is scrolled out of view. */
+[data-offscreen] .terminal :is(.tl, .progress-fill, .cursor) {
+ animation-play-state: paused;
+}
+
/* --- fleet status panel (§05) -----------------------------------------
Sits on the paper Contact section as an inverted panel. Same "tool
panel" vocabulary as .terminal — soft corners, solid rule, hard
@@ -746,7 +764,8 @@ a.hero-status:hover .arrow {
/* Hourly spark strip under each aggregate: one cell per hour of the 24h
window, oldest left, each cell's height its share of the busiest hour
- (--level, 0–1). A zero hour keeps a hairline so the strip reads as a
+ (--level, 0–1) — drawn as a scaleY of a full-height cell, so it animates
+ on the compositor. A zero hour keeps a hairline so the strip reads as a
timeline rather than a gap. */
.fleet-spark {
display: flex;
@@ -760,9 +779,13 @@ a.hero-status:hover .arrow {
.fleet-spark-cell {
flex: 1;
- height: max(1px, calc(var(--level, 0) * 100%));
+ height: 100%;
background: var(--whisper);
- transition: height 0.6s var(--ease-term);
+ /* 0.08 × the strip's 13px content box (14px height minus the 1px
+ border-bottom) ≈ the 1px hairline. Retune if either changes. */
+ transform: scaleY(max(0.08, var(--level, 0)));
+ transform-origin: bottom;
+ transition: transform 0.6s var(--ease-term);
}
@media (prefers-reduced-motion: reduce) {
@@ -1090,13 +1113,17 @@ a.hero-status:hover .arrow {
Phone-only ((FLEET_WINDOW_HOURS).fill(0);
/** One hourly spark strip: a cell per hour, its height the hour's share of
- * the window's busiest hour. */
+ * the window's busiest hour (drawn as a scaleY in globals.css). */
function Spark({ levels }: { levels: number[] }) {
return (
diff --git a/components/site/Footer.tsx b/components/site/Footer.tsx
index b1415fb..5740dea 100644
--- a/components/site/Footer.tsx
+++ b/components/site/Footer.tsx
@@ -6,32 +6,32 @@ export function Footer() {