wip(web): foundation changes left by the stopped website build

Preserved for the restarted website build (whale-road direction). Not verified.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01R9rJEoMuUSRWznU6QjkE7h
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-09-25 03:23:31 -07:00
1 parent 58b1dd3dd3
commit daf8423432
7 files changed
+591 -765

No files matched your search

+1 -1
View File
@@ -102,7 +102,7 @@ def gpui_tokens(data):
"BG": "background", "TEXT": "foreground", "PANEL": "surface",
"TEXT_MUTED": "muted_foreground", "BORDER": "border", "SIDEBAR": "sidebar",
"PRIMARY": "primary", "ON_PRIMARY": "primary_foreground", "ACCENT": "hover",
"LIST_ACTIVE": "selected",
"LIST_ACTIVE": "selected", "ATTENTION": "attention",
}
return [
(f"{prefix}_{name}", tuple(bytes.fromhex(palette[key])))
+37 -74
View File
@@ -7,10 +7,10 @@ html {
}
body {
/* The sheet: one opaque paper field. The descent into the water is drawn
by the waterline band and the ocean column, never by a page gradient. */
background: var(--paper);
color: var(--ink);
/* The twilight floor: one opaque field in the page's appearance. The one
descent into the stage is a hard horizon, never a page gradient. */
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
font-feature-settings: "ss01", "cv11", "tnum";
max-width: 100%;
@@ -27,26 +27,8 @@ main, header, footer, nav { position: relative; z-index: 1; }
with a little negative tracking on the big headings. Code uses the system
monospace stack. No serif, no all-caps, no wide tracking. */
.font-display { font-family: var(--font-display); font-weight: 600; }
.font-condensed { font-family: var(--font-display); font-weight: 600; }
.font-cjk { font-family: var(--font-cjk); }
/* CJK paragraph rhythm — looser leading, wider tracking for body; tighter for headings */
.cjk-body {
line-height: 1.9;
letter-spacing: 0.02em;
word-break: break-all;
}
.cjk-heading {
letter-spacing: -0.01em;
}
.cjk-prose p {
line-height: 1.9;
letter-spacing: 0.02em;
}
/* Full-width punctuation should use CJK spacing */
.cjk-prose {
font-feature-settings: "halt", "pwid";
}
.font-mono { font-family: var(--font-mono); }
/* ---------- the ` · ` chain ---------- */
@@ -166,16 +148,6 @@ html[lang="ko"] h3 {
padding-block: var(--section-pad);
}
.double-rule {
background-image:
linear-gradient(var(--hairline), var(--hairline)),
linear-gradient(var(--hairline), var(--hairline));
background-size: 100% 1px, 100% 1px;
background-position: top, bottom;
background-repeat: no-repeat;
padding: 0.45rem 0;
}
.col-rule > * + * {
border-left: 1px solid var(--hairline);
}
@@ -244,73 +216,64 @@ html[lang="ko"] h3 {
/* ---------- numbers ---------- */
.tabular { font-variant-numeric: tabular-nums; }
.bignum {
font-family: var(--font-body);
font-weight: 600;
font-size: 2.2rem;
line-height: 1;
letter-spacing: -0.04em;
font-variant-numeric: tabular-nums;
}
/* ---------- code blocks ---------- */
/* Code is set into the floor: the recessed surface, no border, the page's
own ink. Prompts and comments recede to muted; keys stay in the text ink
at 500. Nothing here is decorative colour. */
pre.code-block {
background: var(--code-bg);
color: var(--code-fg);
background: var(--surface);
color: var(--text);
max-width: 100%;
min-width: 0;
padding: 1rem 1.1rem;
margin: 0;
padding: var(--space-3) var(--space-4);
font-family: var(--font-mono);
font-size: var(--text-mono);
line-height: 1.55;
border: 1px solid var(--stage-line);
border: 0;
border-radius: var(--radius-surface);
overflow-x: auto;
position: relative;
white-space: pre;
tab-size: 2;
-webkit-overflow-scrolling: touch;
}
pre.code-block::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: var(--hairline);
}
pre.code-block .prompt { color: var(--action-on-dark); }
pre.code-block .comment { color: var(--gpui-stage-ink-dim); }
pre.code-block .key { color: var(--signal-gold); }
@media (max-width: 640px) {
pre.code-block { padding: 0.85rem 0.95rem; }
}
pre.code-block .prompt,
pre.code-block .comment { color: var(--muted); }
pre.code-block .key { color: var(--text); font-weight: 500; }
code.inline {
background: var(--paper-deep);
border: 1px solid var(--hairline);
padding: 0.05rem 0.32rem;
background: var(--surface);
padding: 0.0625rem 0.3125rem;
font-family: var(--font-mono);
font-size: 0.85em;
font-size: 0.875em;
border-radius: var(--radius-control);
overflow-wrap: anywhere;
}
/* ---------- search ---------- */
/* The legacy search input takes the search-field recipe: a recessed
field whose edge identifies it (--line-strong, 3:1), ringed on focus. No
hover colour: only focus goes blue. */
.search-input {
width: 100%;
min-height: 2.5rem;
font-family: var(--font-body);
font-size: 1rem;
padding: 0.75rem 2.5rem 0.75rem 1rem;
background: var(--paper);
color: var(--ink);
border: 1px solid var(--hairline);
transition: border-color var(--dur-state) var(--ease-spring);
font-size: var(--text-body);
padding-block: var(--space-2);
padding-inline: var(--space-3) var(--space-6);
background: var(--surface);
color: var(--text);
border: 1px solid var(--line-strong);
border-radius: var(--radius-control);
transition: border-color var(--dur-state) var(--ease-out);
}
.search-input::placeholder { color: var(--ink-mute); }
.search-input:focus { border-color: var(--ring); }
.search-input::placeholder { color: var(--muted); }
.search-input:focus-visible { border-color: var(--ring); }
mark.search-highlight {
background: var(--indigo-pale);
color: var(--indigo-deep);
padding: 0.02em 0.08em;
border-radius: var(--radius-control);
background: var(--selection);
color: inherit;
}
+167 -205
View File
@@ -1,11 +1,16 @@
/* ---------- nav link ---------- */
/* ------------------------------------------------------------------ */
/* Site chrome — the header, the mobile sheet, the footer stage */
/* ------------------------------------------------------------------ */
/* ---------- header ---------- */
/* An opaque bar on the reading floor with one hairline under it. No glass,
no blur: the page scrolls beneath a solid surface. */
.site-nav {
position: sticky;
z-index: 30;
top: 0;
border-bottom: 1px solid var(--hairline);
background: rgb(var(--c-paper) / 0.94);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
background: var(--bg);
}
.site-nav-inner {
@@ -22,17 +27,18 @@
display: inline-flex;
flex-shrink: 0;
align-items: center;
gap: 0.7rem;
color: var(--ink);
gap: var(--space-2);
color: var(--mark-ink);
font-family: var(--font-display);
font-size: 1.15rem;
font-size: 1.125rem;
font-weight: 600;
letter-spacing: -0.03em;
border-radius: var(--radius-control);
}
.paper-nav-inner {
min-height: 3.85rem;
padding-block: 0.55rem;
min-height: 3.75rem;
padding-block: var(--space-2);
}
.paper-wordmark {
@@ -51,6 +57,8 @@
min-width: 0;
}
/* The mark and the wordmark are one ink, drawn in currentColor, so both
appearances re-ink them without a filter or a second file. */
.paper-wordmark-mark {
display: block;
flex: 0 0 auto;
@@ -58,8 +66,6 @@
height: 22px;
}
/* The traced wordmark is ~7.1:1; when the compact nav clamps its width the
glyphs scale down inside the box instead of squashing. */
.paper-wordmark-logo {
display: block;
flex: 0 1 auto;
@@ -67,39 +73,27 @@
min-width: 0;
max-width: 100%;
height: 20px;
object-fit: contain;
object-position: left center;
}
/* The traced wordmark is navy ink (#142352), which disappears on the dark
scheme's charcoal. Under dark (OS or pinned) it is re-inked to the light
stage foreground; the footer already uses the reversed cut. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .paper-wordmark-logo {
filter: brightness(0) invert(0.93);
}
}
:root[data-theme="dark"] .paper-wordmark-logo {
filter: brightness(0) invert(0.93);
}
/* The one primary action in the header. It keeps its class for layout;
its paint is the .btn-primary recipe. */
.paper-install-cta {
align-items: center;
justify-content: center;
min-height: 2.25rem;
padding: 0.35rem 0.85rem;
background: var(--indigo);
color: var(--paper);
padding-inline: var(--space-3);
background: var(--accent);
color: var(--on-accent);
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 600;
border-radius: var(--radius-control);
transition: background-color var(--dur-state) var(--ease-spring);
transition: background-color var(--dur-state) var(--ease-out);
}
.paper-install-cta:hover {
background: var(--indigo-deep);
color: var(--paper);
color: var(--on-accent);
}
/* The one identity door: a quiet text control beside the primary action. */
@@ -107,13 +101,13 @@
align-items: center;
justify-content: center;
min-height: 2.25rem;
padding: 0.35rem 0.75rem;
color: var(--ink);
padding-inline: var(--space-3);
color: var(--text);
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 600;
border-radius: var(--radius-control);
transition: background-color var(--dur-state) var(--ease-spring);
transition: background-color var(--dur-state) var(--ease-out);
}
.paper-auth-signin:hover { background: var(--hover); }
@@ -128,12 +122,12 @@
}
.site-nav-actions > * { flex-shrink: 0; }
.site-nav-actions > .paper-auth-signin { margin-left: 0.5rem; }
.site-nav-actions > .paper-install-cta { margin-left: 0.25rem; }
.site-nav-actions > .paper-auth-signin { margin-inline-start: 0.5rem; }
.site-nav-actions > .paper-install-cta { margin-inline-start: 0.25rem; }
/* Quiet icon controls (theme, language, GitHub, menu): a 40px target around
a 20px glyph, the GPUI rail metrics. Muted at rest, full ink on hover and
while the control is expanded; the accessible name carries the state. */
/* Quiet icon controls: a 40px target around a 20px glyph, the GPUI rail
metrics. Muted at rest, full ink on hover and while expanded; a pressed
or current control takes the neutral selected fill. */
.nav-icon-button {
position: relative;
display: inline-flex;
@@ -141,16 +135,38 @@
height: 2.5rem;
align-items: center;
justify-content: center;
color: var(--ink-soft);
color: var(--muted);
border-radius: var(--radius-control);
cursor: pointer;
transition: background-color var(--dur-state) var(--ease-spring), color var(--dur-state) var(--ease-spring);
transition-property: background-color, color, box-shadow;
transition-duration: var(--dur-state);
transition-timing-function: var(--ease-out);
}
.nav-icon-button:hover,
.nav-icon-button[aria-expanded="true"] {
background: var(--hover);
color: var(--ink);
color: var(--text);
}
.nav-icon-button[aria-pressed="true"],
.nav-icon-button[aria-current="page"] {
background: var(--selected);
color: var(--text);
}
.nav-icon-button:active { opacity: 0.75; }
.nav-icon-button:focus-visible { box-shadow: var(--glow-focus); }
/* The small variant for controls inside rows and code blocks; it grows to
the touch floor on coarse pointers. */
.nav-icon-button-sm {
width: 2rem;
height: 2rem;
}
.nav-icon-button-sm .nav-icon { width: 1rem; height: 1rem; }
@media (pointer: coarse) {
.nav-icon-button-sm { width: 2.75rem; height: 2.75rem; }
}
.nav-icon { display: block; width: 1.25rem; height: 1.25rem; }
@@ -169,12 +185,11 @@
}
.nav-locale:has(select:focus-visible) {
box-shadow: 0 0 0 var(--gpui-focus-width) var(--ring);
box-shadow: 0 0 0 var(--gpui-focus-width) var(--ring), var(--glow-focus);
}
/* Compact sheet rows: one label per destination, a chevron, a selected fill
for the current page rather than a decorative bar. */
.mm-list > li + li { border-top: 1px solid var(--hairline); }
/* ---------- the mobile sheet ---------- */
.mm-list > li + li { border-top: 1px solid var(--line); }
.mm-link {
display: flex;
@@ -182,15 +197,15 @@
gap: 0.75rem;
min-height: 3.25rem;
padding-inline: 0.5rem;
color: var(--ink);
color: var(--text);
font-size: 1.0625rem;
border-radius: var(--radius-control);
transition: background-color var(--dur-state) var(--ease-spring);
transition: background-color var(--dur-state) var(--ease-out);
}
.mm-link:hover { background: var(--hover); }
.mm-link[aria-current="page"] { background: var(--selected); }
.mm-link-chevron { margin-left: auto; color: var(--ink-mute); }
.mm-link-chevron { margin-inline-start: auto; color: var(--muted); }
.mm-action {
display: flex;
@@ -201,51 +216,63 @@
.mm-action.paper-install-cta { margin-top: 1.5rem; }
/* ---------- primary nav links ---------- */
/* Hover is a neutral wash. The current page keeps the 2px accent bar under
its label: the active-tab grammar, the one place blue marks "here". */
.nav-link {
font-family: var(--font-body);
font-size: 0.78rem;
font-size: 0.8125rem;
letter-spacing: 0;
color: var(--ink);
color: var(--text);
position: relative;
padding: 0.25rem 0;
padding: 0.375rem 0.5rem;
margin-inline: -0.5rem;
border-radius: var(--radius-control);
transition: background-color var(--dur-state) var(--ease-out);
}
.nav-link:hover { background: var(--hover); }
.nav-link::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: -2px;
inset-inline: 0.5rem;
bottom: -2px;
height: 2px;
background: var(--indigo);
transform: scaleX(0);
transform-origin: left;
transition: transform var(--dur-state) var(--ease-spring);
opacity: 0;
}
.nav-link:hover::after, .nav-link[aria-current="page"]::after { transform: scaleX(1); }
.nav-link[aria-current="page"]::after { opacity: 1; }
/* ---------- footer ---------- */
/* Compact viewports: the strip keeps the home control clickable. These
rules also exist in home.css until the home slice deletes them there. */
@media (max-width: 520px) {
.site-nav-inner {
gap: 0.5rem;
}
.site-nav-actions {
min-width: 0;
}
.paper-wordmark {
max-width: 9.75rem;
}
}
/* ---------- footer — the stage below the horizon ---------- */
/* The footer is midnight in both appearances (tokens-roles.css). Its top
edge is the page's one horizon: two flat grounds meeting at a pixel, plus
a hairline only where they are too close to tell apart (dark). */
.site-footer {
position: relative;
background: var(--ocean-deep);
color: var(--stage-soft);
border-top: 1px solid var(--horizon-edge);
background: var(--bg);
color: var(--muted);
}
/* The waterline before the seabed. Every page that is still on paper when
it reaches the footer descends here; the homepage is already under water
by then, so its column hides the band and the seabed simply continues. */
.site-footer-waterline {
position: relative;
height: clamp(9rem, 18vw, 15rem);
overflow: hidden;
background: var(--paper);
}
.site-footer-waterline > svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
main:has(.ocean-column) + .site-footer .site-footer-waterline { display: none; }
.site-footer-main { border-top: 1px solid rgb(var(--c-stage-soft) / 0.13); }
main:has(.ocean-column) + .site-footer .site-footer-main { border-top-color: transparent; }
/* A page whose main already ends on the stage continues into the footer
with no second edge. */
main:has(> .midnight:last-child) + .site-footer,
main:has(> .ocean-column) + .site-footer { border-top-color: transparent; }
.site-footer-main,
.site-footer-meta {
@@ -255,151 +282,86 @@ main:has(.ocean-column) + .site-footer .site-footer-main { border-top-color: tra
.site-footer-main {
display: grid;
grid-template-columns: minmax(16rem, 1.25fr) minmax(18rem, 0.75fr);
gap: 4rem;
padding-block: 2.8rem;
grid-template-columns: minmax(14rem, 1fr) minmax(0, 2fr);
gap: var(--space-7);
padding-block: var(--space-7) var(--space-6);
}
.site-wordmark-footer { color: var(--stage-text); }
.site-wordmark-footer img { display: block; width: auto; height: 18px; }
.site-footer-brand p { max-width: 30rem; margin-top: 0.8rem; color: rgb(var(--c-stage-soft) / 0.62); font-size: 0.86rem; line-height: 1.6; }
.site-footer-brand { display: grid; gap: var(--space-3); align-content: start; }
.site-footer-brand p {
max-width: 28rem;
color: var(--muted);
font-size: var(--text-ui);
line-height: 1.5;
}
.site-wordmark-footer { color: var(--text); justify-self: start; }
.site-footer-links {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: var(--space-5);
}
.site-footer-links > div { display: flex; flex-direction: column; gap: 0.55rem; }
.site-footer-links span { margin-bottom: 0.25rem; color: var(--ocean-current); font-family: var(--font-body); font-size: 0.75rem; }
.site-footer-links a { color: rgb(var(--c-stage-soft) / 0.76); font-size: 0.82rem; }
.site-footer-links a:hover { color: var(--stage-text); }
.site-footer-group { display: grid; gap: var(--space-2); align-content: start; }
.site-footer-label {
color: var(--muted);
font-size: var(--text-caption);
font-weight: 500;
}
.site-footer-list { display: grid; gap: 2px; }
.site-footer-list a {
display: inline-flex;
align-items: center;
min-height: 1.75rem;
padding-inline: var(--space-2);
margin-inline: calc(-1 * var(--space-2));
color: var(--text);
font-size: var(--text-ui);
border-radius: var(--radius-control);
transition: background-color var(--dur-state) var(--ease-out);
}
.site-footer-list a:hover { background: var(--hover); }
.site-footer-meta {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 1.5rem;
display: flex;
flex-wrap: wrap;
align-items: center;
padding-block: 1rem;
border-top: 1px solid rgb(var(--c-stage-soft) / 0.13);
color: rgb(var(--c-stage-soft) / 0.5);
font-family: var(--font-mono);
font-size: 0.75rem;
justify-content: space-between;
gap: var(--space-3) var(--space-5);
padding-block: var(--space-4) var(--space-6);
border-top: 1px solid var(--line);
color: var(--muted);
font-size: var(--text-caption);
}
.site-footer-meta a:hover { color: var(--ocean-current); }
.site-footer-meta > div { display: flex; gap: 1rem; }
.site-footer-meta > p { min-width: 0; max-width: 70ch; overflow-wrap: anywhere; }
.site-footer-meta p { min-width: 0; max-width: 70ch; overflow-wrap: anywhere; }
.site-footer-meta a {
color: var(--text);
text-decoration: underline;
text-decoration-color: var(--line-strong);
text-underline-offset: 0.2em;
}
.site-footer-meta a:hover { text-decoration-color: currentColor; }
/* ---------- ticker ---------- */
.ticker-viewport {
position: relative;
flex: 1 1 auto;
overflow: hidden;
.site-footer-meta-links {
display: flex;
flex-wrap: wrap;
gap: var(--space-2) var(--space-4);
}
.ticker-track {
display: inline-flex;
gap: 3rem;
white-space: nowrap;
animation: ticker 80s linear infinite;
padding-right: 3rem;
}
@keyframes ticker {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
@media (max-width: 900px) {
.site-footer-main { grid-template-columns: 1fr; gap: var(--space-6); }
}
/* One wire entry: verb, number or tag, title, by-line, age. Baseline-aligned
so the mono verb sits on the same line as the title it qualifies. */
.ticker-item {
display: inline-flex;
align-items: baseline;
gap: 0.5rem;
@media (forced-colors: active) {
.site-footer { border-top-color: CanvasText; }
}
/* The verb carries the state. Merges are the headline case, so they get the
one warm-neutral ink on the sheet; releases take ochre; anything closed
recedes to mute. Everything else is the action blue. */
.ticker-verb {
color: var(--indigo);
font-size: 0.75rem;
}
.ticker-verb[data-event="merged"] { color: var(--jade); }
.ticker-verb[data-event="published"] { color: var(--ochre); }
.ticker-verb[data-event="closed"] { color: var(--ink-mute); }
.ticker-num,
.ticker-tag,
.ticker-by,
.ticker-age {
color: var(--ink-mute);
}
.ticker-title {
color: var(--ink);
max-width: 24rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The contributor's handle is the point of the line, not a footnote. */
.ticker-handle {
color: var(--ink);
font-weight: 600;
}
.ticker-first {
padding: 0 0.35rem;
border: 1px solid var(--ochre);
color: var(--ochre);
font-size: 0.75rem;
}
.ticker-sep { color: var(--paper-line-soft); }
@media (prefers-reduced-motion: reduce) {
.ticker-track { animation: none; }
/* A frozen track would clip every entry past the fold with no way to reach
them. Stopping the motion must not also remove the content. */
.ticker-viewport { overflow-x: auto; }
}
/* ---------- ambient ---------- */
/*
* One effect, OPT-IN under `no-preference` rather than opted out under
* `reduce`. A `reduce` override can be defeated by specificity; a gate cannot.
* Reduced motion freezes the field — it never removes it, so the column's
* gradient and the whale below are unconditional and only the movement is not.
* The whale mark itself is always the still pose.
*
* The TUI's ambient life — fish, jellyfish, bubbles, the whale cameo — stays
* out of scope. This page quotes the product's restraint doctrine in its own
* copy. The standing loops on the whole site are three — ticker, caret, and
* this one. Entrances are transitions, and the one-shot settles live in the
* Motion section below under the same no-preference gate.
*/
/* M1 — the column breath. Opacity only, on a 90s cycle: the TUI's authored
0.018–0.055 phase bias expressed as the same fraction of a mix. Opacity is
the one property that cannot cause layout work on an element four bands
tall. No position change, no hue shift, no filter. */
@keyframes cw-breath {
0%, 100% { opacity: 0; }
50% { opacity: 0.045; }
}
@media (prefers-reduced-motion: no-preference) {
.ocean-column::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background: var(--stage-ambient);
opacity: 0;
animation: cw-breath 90s ease-in-out infinite;
}
@media print {
.site-footer { border-top-color: currentColor; background: none; color: inherit; }
}
+50 -136
View File
@@ -1,187 +1,101 @@
/* ------------------------------------------------------------------ */
/* Shared surface states — empty / loading / error (surface-state.tsx) */
/* Route boundaries and the connection banner */
/* ------------------------------------------------------------------ */
/*
* One plate for every data-bearing page's non-data moments. The mark on the
* left carries the state in colour AND the copy carries it in words, so no
* state is conveyed by colour alone: mute for empty, action blue for loading,
* coral for error — the status-bar grammar the product already speaks.
* The empty / loading / error grammar itself lives in primitives.css
* (.empty-state, .skeleton-bar); components/surface-state.tsx renders it.
* This file keeps the two containers around it.
*/
.state-block {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 1rem;
align-items: start;
padding: clamp(1.5rem, 4vw, 2.25rem) clamp(1.25rem, 3vw, 1.75rem);
border: 1px solid var(--stage-line);
border-radius: var(--radius-control);
background: var(--paper-card);
color: var(--ink);
}
.state-block-compact {
padding: 1rem 1.15rem;
gap: 0.8rem;
}
.state-mark {
width: 0.6rem;
height: 0.6rem;
margin-top: 0.42rem;
border-radius: var(--radius-pill);
background: var(--ink-mute);
flex: none;
}
.state-block-loading .state-mark { background: var(--indigo); }
.state-block-error .state-mark { background: var(--ocean-coral); }
.state-block-error { border-color: color-mix(in srgb, var(--ocean-coral) 45%, var(--stage-line)); }
.state-copy { min-width: 0; }
.state-title {
color: var(--ink);
font-size: 0.98rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.state-body {
max-width: 44rem;
margin-top: 0.4rem;
color: var(--ink-soft);
font-size: 0.86rem;
line-height: 1.65;
}
.state-actions {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-top: 1rem;
}
.state-retry[aria-busy="true"] { opacity: 0.7; cursor: progress; }
/* Skeleton lines: neutral bars, never fake words. The shimmer is opt-in
under no-preference; reduced motion sees still bars. */
.state-skeleton {
display: grid;
gap: 0.55rem;
margin-top: 0.85rem;
}
.state-skeleton > span {
display: block;
height: 0.7rem;
border-radius: var(--radius-control);
background: color-mix(in srgb, var(--ink-mute) 22%, transparent);
}
@media (prefers-reduced-motion: no-preference) {
.state-skeleton > span {
background: linear-gradient(
90deg,
color-mix(in srgb, var(--ink-mute) 18%, transparent) 0%,
color-mix(in srgb, var(--ink-mute) 34%, transparent) 50%,
color-mix(in srgb, var(--ink-mute) 18%, transparent) 100%
);
background-size: 200% 100%;
animation: state-shimmer 1.6s ease-in-out infinite;
}
}
@keyframes state-shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
/* Route-level boundaries (loading.tsx / error.tsx / not-found.tsx) sit in
the site container with the page rhythm so a boundary never looks like a
the site container with the page rhythm, so a boundary never looks like a
different site. */
.route-state {
width: var(--container);
margin-inline: auto;
padding-block: clamp(3rem, 6vw, 4.75rem);
padding-block: var(--space-6) var(--space-8);
max-width: 52rem;
}
/* ------------------------------------------------------------------ */
/* Connection banner — offline / reconnecting / degraded / restored */
/* ------------------------------------------------------------------ */
/* A raised sheet with a dot and a word. The dot's hue is the state
vocabulary (danger offline, accent reconnecting, attention degraded, live
restored) and the title always says it in words. No blur, no pulse. */
.connection-banner {
position: sticky;
top: 0;
z-index: 35;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.9rem;
gap: var(--space-3);
align-items: center;
width: var(--container);
margin: 0.75rem auto 0;
padding: 0.7rem 1rem;
border: 1px solid var(--stage-line);
border-radius: var(--radius-control);
background: color-mix(in srgb, var(--paper-card) 92%, transparent);
backdrop-filter: blur(8px);
color: var(--ink);
margin: var(--space-3) auto 0;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--line);
border-radius: var(--radius-surface);
background: var(--panel);
box-shadow: var(--shadow-popover);
color: var(--text);
}
.connection-mark {
width: 0.6rem;
height: 0.6rem;
width: 0.5rem;
height: 0.5rem;
border-radius: var(--radius-pill);
background: var(--ink-mute);
background: currentColor;
color: var(--muted);
}
.connection-banner-offline { border-color: color-mix(in srgb, var(--ocean-coral) 50%, var(--stage-line)); }
.connection-banner-offline .connection-mark { background: var(--ocean-coral); }
.connection-banner-reconnecting .connection-mark { background: var(--indigo); }
.connection-banner-degraded .connection-mark { background: var(--ochre); }
.connection-banner-restored { border-color: color-mix(in srgb, var(--jade) 50%, var(--stage-line)); }
.connection-banner-restored .connection-mark { background: var(--jade); }
.connection-banner-offline .connection-mark { color: var(--danger); }
.connection-banner-reconnecting .connection-mark { color: var(--accent); box-shadow: var(--glow); }
.connection-banner-degraded .connection-mark { color: var(--attention); box-shadow: var(--glow); }
.connection-banner-restored .connection-mark { color: var(--live); }
@media (prefers-reduced-motion: no-preference) {
.connection-banner-reconnecting .connection-mark {
animation: connection-pulse 1.2s ease-in-out infinite;
}
.connection-banner {
animation: mm-in var(--dur-spring) var(--ease-spring) both;
animation: mm-in var(--dur-arrive) var(--ease-out) both;
}
}
@keyframes connection-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.connection-copy { min-width: 0; }
.connection-title { font-size: 0.86rem; font-weight: 600; }
.connection-body { margin-top: 0.15rem; color: var(--ink-soft); font-size: 0.78rem; line-height: 1.5; }
.connection-checked { color: var(--ink-mute); font-family: var(--font-mono); font-size: 0.75rem; }
.connection-title { font-size: var(--text-ui); font-weight: 500; }
.connection-body { margin-top: 0.125rem; color: var(--muted); font-size: var(--text-caption); line-height: 1.5; }
.connection-checked { color: var(--muted); }
.connection-actions { display: flex; gap: 0.5rem; }
.connection-actions { display: flex; gap: var(--space-2); }
/* The banner's buttons take the .btn secondary and primary paint under
the class names the component emits. */
.connection-button {
display: inline-flex;
min-height: 2.25rem;
min-height: 2rem;
align-items: center;
padding: 0.35rem 0.75rem;
border: 1px solid var(--stage-line);
padding-inline: var(--space-3);
border: 1px solid var(--line);
border-radius: var(--radius-control);
color: var(--ink);
background: var(--panel);
color: var(--text);
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 600;
transition: border-color var(--dur-state) var(--ease-spring), background-color var(--dur-state) var(--ease-spring), color var(--dur-state) var(--ease-spring);
font-size: var(--text-ui);
font-weight: 500;
transition-property: background-color, border-color, color;
transition-duration: var(--dur-state);
transition-timing-function: var(--ease-out);
}
.connection-button:hover { border-color: var(--indigo); color: var(--indigo); }
.connection-button-primary { border-color: var(--indigo); background: var(--indigo); color: var(--paper); }
.connection-button-primary:hover { background: var(--indigo-deep); border-color: var(--indigo-deep); color: var(--paper); }
.connection-button:disabled { opacity: 0.55; cursor: not-allowed; }
.connection-button:hover { background: var(--hover); }
.connection-button-primary { border-color: transparent; background: var(--accent); color: var(--on-accent); }
.connection-button-primary:hover { background: var(--accent-hover); color: var(--on-accent); }
.connection-button:disabled { opacity: 0.5; cursor: not-allowed; }
.connection-button[aria-busy="true"] { cursor: progress; }
@media (pointer: coarse) {
.connection-button { min-height: 2.75rem; }
}
@media (max-width: 560px) {
.connection-banner {
grid-template-columns: auto minmax(0, 1fr);
+298 -183
View File
@@ -1,58 +1,193 @@
/* ---------- site tokens — the folio ---------- */
/* ------------------------------------------------------------------ */
/* Tokens and roles — the whale-road */
/* ------------------------------------------------------------------ */
/*
* The site follows the OS appearance, exactly as the GPUI client follows its
* `set_theme` pair: the GPUI light theme (warm paper) by default, the GPUI
* dark theme (warm charcoal) under `prefers-color-scheme: dark` unless the
* page pins `data-theme="light"`, and dark whenever it pins
* `data-theme="dark"`. The pin comes from the theme toggle (every page), and
* the inline boot script in the locale layout applies it before paint.
* One token authority: vendor/codewhale-design/tokens.json, generated into
* ../tokens.css as `--gpui-light-*` / `--gpui-dark-*`. This file names the
* roles every surface paints with (codewhale-design/DIRECTION.md):
*
* The homepage water column (`.ocean-column`) and the footer seabed
* (`.site-footer`) are the stage: a fixed dark material in both schemes, like
* a terminal plate, because the waterline strata that descend into them are
* drawn in the stage's own tokens. They share the one dark block below.
* Depth is elevation.
* sunlit --panel raised plates, popovers, the thing in focus
* twilight --bg the reading floor
* recessed --surface code, rails, chrome set into the floor
* midnight .midnight the stage below the one horizon (the footer,
* a home descent); always the dark set, in both
* appearances
*
* The token NAMES keep their historical shape (`--paper`, `--ink`,
* `--indigo`) so every component rule stays diffable; the VALUES resolve
* through the `--gpui-*` aliases below, which reference the generated GPUI
* tokens from vendor/codewhale-design/tokens.json. Only the warm state hues (tan, moss, rust and their AA-adapted
* inks) stay literal: `set_theme` has no slot for them.
* Bioluminescence is how things light up. Focus, live work and "needs
* you" carry a soft halo (--glow, --glow-focus): a point of light in the
* dark, quieter on paper. Nothing else glows.
*
* State is four hues and always a word: --accent (action, focus),
* --live (working, done, verified), --attention (needs you, warning),
* --danger (failed, destructive). Idle and offline use --muted.
*
* NAMING TRAP: tokens.json `surface` is the RAISED colour (web --panel);
* tokens.json `sidebar` is the RECESSED one (web --surface).
*
* Every role is declared in each scheme block — light (:root), OS-dark and
* pinned-dark (identical), and midnight — because a custom property that
* references another is computed where it is declared.
*
* The legacy names (--paper, --ink, --indigo, --stage-*, --ocean-*, ...)
* stay only so pages that have not migrated keep painting; new work uses the
* roles. They are removed in the post-renovation sweep.
*/
/* ---------- scheme-independent tokens ---------- */
:root {
/* --- GPUI light (above the waterline) --- */
--gpui-paper: var(--gpui-light-bg); /* background — warm paper */
--gpui-paper-raised: var(--gpui-light-panel); /* muted — card/popover */
--gpui-paper-muted: var(--gpui-light-sidebar); /* sidebar — recessed */
--gpui-ink: var(--gpui-light-text); /* foreground */
--gpui-ink-soft: var(--gpui-light-text-muted); /* muted_foreground */
--gpui-ink-mute: var(--gpui-light-text-muted); /* muted_foreground */
--gpui-edge: var(--gpui-light-border); /* border */
--gpui-primary: var(--gpui-light-primary); /* primary */
--gpui-primary-hover: rgb(var(--gpui-light-primary-rgb) / var(--gpui-primary-hover-opacity)); /* button_primary_hover */
/* State and control-edge hues that tokens.json 1.0.0 has no slot for.
Values and their contrast on all five grounds are measured in
codewhale-design/reference/MEASUREMENTS.md §2.2–2.3 (proposed
tokens.json 1.1.0). Light live #3a6352 ≥ 4.97:1, light danger #9e3f36
≥ 4.77:1, dark live #9ec7b2 ≥ 6.21:1, dark danger #e39a90 ≥ 5.12:1 on
background, panel, sidebar, hover and selected. border-strong clears
the 3:1 non-text floor on the same five grounds. */
--gpui-light-live: #3a6352;
--gpui-dark-live: #9ec7b2;
--gpui-light-danger: #9e3f36;
--gpui-dark-danger: #e39a90;
--gpui-light-border-strong: #807c76;
--gpui-dark-border-strong: #828386;
/* --- GPUI dark (below the waterline) --- */
--gpui-stage: var(--gpui-dark-bg); /* background — warm charcoal */
--gpui-stage-deep: var(--gpui-dark-sidebar); /* sidebar — deepest dark */
--gpui-stage-muted: var(--gpui-dark-panel); /* muted — raised input plate */
--gpui-stage-raised: var(--gpui-dark-list-active); /* list_active — top plate */
--gpui-stage-ink: var(--gpui-dark-text); /* foreground */
--gpui-stage-ink-soft: var(--gpui-dark-text-muted); /* muted_foreground */
--gpui-stage-ink-dim: var(--gpui-dark-text-muted); /* muted_foreground (AA, UX-05) */
--gpui-stage-edge: var(--gpui-dark-border); /* border */
--gpui-primary-dark: var(--gpui-dark-primary); /* primary */
--gpui-primary-hover-dark: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity)); /* button_primary_hover */
--gpui-accent-dark: var(--gpui-dark-accent); /* accent — hover wash */
--gpui-selection-dark: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity)); /* selection */
/* Legacy GPUI aliases, still read by unmigrated page partials. */
--gpui-paper: var(--gpui-light-bg);
--gpui-paper-raised: var(--gpui-light-panel);
--gpui-paper-muted: var(--gpui-light-sidebar);
--gpui-ink: var(--gpui-light-text);
--gpui-ink-soft: var(--gpui-light-text-muted);
--gpui-ink-mute: var(--gpui-light-text-muted);
--gpui-edge: var(--gpui-light-border);
--gpui-primary: var(--gpui-light-primary);
--gpui-primary-hover: rgb(var(--gpui-light-primary-rgb) / var(--gpui-primary-hover-opacity));
--gpui-stage: var(--gpui-dark-bg);
--gpui-stage-deep: var(--gpui-dark-sidebar);
--gpui-stage-muted: var(--gpui-dark-panel);
--gpui-stage-raised: var(--gpui-dark-list-active);
--gpui-stage-ink: var(--gpui-dark-text);
--gpui-stage-ink-soft: var(--gpui-dark-text-muted);
--gpui-stage-ink-dim: var(--gpui-dark-text-muted);
--gpui-stage-edge: var(--gpui-dark-border);
--gpui-primary-dark: var(--gpui-dark-primary);
--gpui-primary-hover-dark: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
--gpui-accent-dark: var(--gpui-dark-accent);
--gpui-selection-dark: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
--gpui-tan: #b1a17a;
--gpui-moss: #739686;
--gpui-rust: #aa6f6c;
--c-stage-soft: var(--gpui-dark-text-muted-rgb);
--c-stage-deep: var(--gpui-dark-sidebar-rgb);
--c-primary-dark: var(--gpui-dark-primary-rgb);
/* Warm-muted state hues from the GPUI mockups, darkened or lifted per
side of the waterline so text uses of them still clear AA. No
set_theme slots: these are site-specific adaptations. */
--gpui-tan: #b1a17a; /* human mark */
--gpui-moss: #739686; /* outcome */
--gpui-rust: #aa6f6c; /* attention */
/* Type: Shannon Sans for every role, the system monospace stack for
things a person could type or paste. No serif, no third family. */
--font-body: var(--font-shannon-latin), var(--font-shannon-ext), var(--gpui-font-fallbacks),
ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-display: var(--font-body);
--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
--font-cjk: var(--gpui-font-fallbacks), sans-serif;
/* RGB channel triples backing the Tailwind surface/ink tokens (see
tailwind.config.ts). The dark block below re-points them. */
/* Type scale (MEASUREMENTS §3.1). 12px is the floor: nothing renders
smaller. Display sizes are website-only, one per page. */
--text-caption: 0.75rem; /* 12/16 meta, timestamps, labels */
--text-mono: var(--gpui-mono-size); /* 13/20 code, commands */
--text-ui: 0.875rem; /* 14/20 controls, rows, panel body */
--text-body: 1rem; /* 16/24 short copy, form inputs (no iOS zoom) */
--text-prose: 1.0625rem; /* 17/26 reading: docs, legal, answers */
--leading-prose: 1.53;
--text-heading: 1.0625rem; /* 17/24 600 section heading */
--text-title: 1.375rem; /* 22/28 600 page or sheet title */
--text-display: clamp(1.75rem, 1.2rem + 2.2vw, 2.5rem); /* 28→40 inner-page hero */
--text-display-lg: clamp(2.5rem, 1.6rem + 3.6vw, 3.5rem); /* 40→56 the home hero */
/* Short aliases the spec uses. */
--text-xs: var(--text-caption);
--text-sm: var(--text-ui);
--text-base: var(--text-body);
/* Space: a 4px program (MEASUREMENTS §4.1). */
--space-1: 0.25rem; /* 4 */
--space-2: 0.5rem; /* 8 */
--space-3: 0.75rem; /* 12 */
--space-4: 1rem; /* 16 */
--space-5: 1.5rem; /* 24 */
--space-6: 2rem; /* 32 */
--space-7: 3rem; /* 48 */
--space-8: 4rem; /* 64 */
--section-gap: clamp(var(--space-7), 6vw, var(--space-8));
/* One container, one reading measure. --measure-prose is the GPUI
PROSE_MEASURE (880px), not the 1440px READING_COLUMN. */
--container: min(100% - var(--gpui-spacing-page), 76rem);
--measure-prose: 55rem;
--measure-text: 70ch;
--hero-pad: clamp(3rem, 6vw, 4.75rem);
--section-pad: clamp(2.75rem, 5vw, 4rem);
/* Radius: 6 controls, 10 surfaces (code, menus, cards), 14 sheets,
pill for dots. No other radius. */
--radius-control: var(--gpui-radius-control);
--radius-surface: var(--gpui-radius-panel);
--radius-sheet: 14px;
--radius-pill: 999px;
/* Motion (MEASUREMENTS §9.1). Nothing bounces: every curve stays inside
[0, 1]. --ease-spring is the GPUI PANEL_SPRING fit (no overshoot). */
--dur-state: 120ms; /* colour, border, opacity on hover and press */
--dur-arrive: 180ms; /* something new arrives, rise <= 4px */
--dur-exit: 120ms; /* something closes or leaves */
--dur-spring: 340ms; /* panes and sheets */
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-spring: cubic-bezier(0.2, 0.1, 0.16, 1);
--ease-exit: var(--ease-in);
}
/* Reduced motion is a still pose: every duration steps. Keyframed motion is
additionally declared only inside `prefers-reduced-motion:
no-preference`, so it never starts. */
@media (prefers-reduced-motion: reduce) {
:root {
--dur-state: 0ms;
--dur-spring: 0ms;
--dur-arrive: 0ms;
--dur-exit: 0ms;
}
}
/* ---------- light — the twilight floor on warm paper ---------- */
:root {
--bg: var(--gpui-light-bg);
--surface: var(--gpui-light-sidebar);
--panel: var(--gpui-light-panel);
--text: var(--gpui-light-text);
--muted: var(--gpui-light-text-muted);
--line: var(--gpui-light-border);
--line-strong: var(--gpui-light-border-strong);
--accent: var(--gpui-light-primary);
--accent-hover: rgb(var(--gpui-light-primary-rgb) / var(--gpui-primary-hover-opacity));
--on-accent: var(--gpui-light-on-primary);
--hover: var(--gpui-light-accent);
--selected: var(--gpui-light-list-active);
--selection: rgb(var(--gpui-light-primary-rgb) / var(--gpui-selection-opacity));
--ring: var(--gpui-light-primary);
--live: var(--gpui-light-live);
--success: var(--gpui-light-live);
--attention: var(--gpui-light-attention);
--danger: var(--gpui-light-danger);
--on-danger: var(--gpui-light-on-primary);
--diff-add: rgb(104 152 119 / 0.14);
--diff-del: rgb(189 119 116 / 0.11);
--mark-ink: var(--gpui-light-text);
/* Light in the dark: on paper the halo is a quiet ring. */
--glow: 0 0 0 3px color-mix(in srgb, currentColor 14%, transparent);
--glow-focus: 0 0 0 6px rgb(var(--gpui-light-primary-rgb) / 0.12);
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.16);
/* The horizon needs a drawn edge only where the two grounds are within
1.2:1 of each other — the dark appearance. */
--horizon-edge: transparent;
/* Legacy names (sweep). */
--c-paper: var(--gpui-light-bg-rgb);
--c-paper-deep: var(--gpui-light-sidebar-rgb);
--c-paper-edge: var(--gpui-light-border-rgb);
@@ -60,11 +195,6 @@
--c-ink-soft: var(--gpui-light-text-muted-rgb);
--c-ink-mute: var(--gpui-light-text-muted-rgb);
--c-indigo: var(--gpui-light-primary-rgb);
--c-stage-soft: var(--gpui-dark-text-muted-rgb);
--c-stage-deep: var(--gpui-dark-sidebar-rgb);
--c-primary-dark: var(--gpui-dark-primary-rgb);
/* The sheet. */
--paper: var(--gpui-paper);
--paper-deep: var(--gpui-paper-muted);
--paper-edge: var(--gpui-edge);
@@ -74,28 +204,21 @@
--ink: var(--gpui-ink);
--ink-soft: var(--gpui-ink-soft);
--ink-mute: var(--gpui-ink-mute);
/* Action on paper is the GPUI light primary; hover sinks to its hover. */
--indigo: var(--gpui-primary);
--indigo-deep: var(--gpui-primary-hover);
--indigo-pale: rgb(var(--c-indigo) / 0.1);
--action-on-dark: var(--gpui-primary-dark);
/* State inks darkened from the mockup hues so they clear AA on paper:
moss is outcome, rust is attention, tan is the human mark. */
--ochre: #7d6a3f;
--jade: #4e6f61;
--seafoam: var(--gpui-moss);
--cobalt: var(--gpui-primary);
--cyan: var(--gpui-primary); /* reserved for the composer prompt glyph and the release line */
--cyan: var(--gpui-primary);
--ocean-deep: var(--gpui-stage-deep);
--ocean-mid: var(--gpui-stage-muted);
--ocean-current: var(--gpui-primary-dark);
--ocean-mist: var(--gpui-stage-ink-soft);
--ocean-coral: #8f5a56;
--signal-gold: #7d6a3f;
/* The whale mark is ink on paper; below the waterline it is the light
stage foreground. The warm tan stays reserved for human moments. */
--mark-ink: var(--gpui-ink);
/* Docs sheet inks: the sidebar's current link and the secondary button. */
--docs-accent: var(--gpui-primary);
--docs-button-bg: var(--gpui-paper-raised);
--docs-button-border: var(--gpui-edge);
@@ -103,124 +226,23 @@
--stage-text: var(--gpui-stage-ink);
--stage-soft: var(--gpui-stage-ink-soft);
--stage-muted: var(--gpui-stage-ink-dim);
/* The water column, re-inked in the GPUI dark ramp. */
--stage-field-top: var(--gpui-stage-muted); /* surface */
--stage-field-mid: var(--gpui-stage); /* the authored 42% break */
--stage-field-deep: var(--gpui-stage-deep); /* deep field, and the footer seabed */
--stage-field-top: var(--gpui-stage-muted);
--stage-field-mid: var(--gpui-stage);
--stage-field-deep: var(--gpui-stage-deep);
--stage-ambient: var(--gpui-accent-dark);
--stage-composer: var(--gpui-stage-muted); /* the raised input plate */
--stage-composer: var(--gpui-stage-muted);
--stage-elevated: var(--gpui-stage-raised);
--stage-line: var(--gpui-stage-edge); /* the charcoal border on stage */
--stage-line: var(--gpui-stage-edge);
--stage-hint: var(--gpui-stage-ink-dim);
--stage-dim: var(--gpui-stage-ink-soft);
--violet: var(--gpui-selection-dark); /* 1px rules only, never text */
/* Type, as GPUI set_theme: Shannon Sans for every role (the Latin and
extended subsets load in app/[locale]/layout.tsx), the system monospace
stack at 13px for code, prose at 15/23. Han, kana and
Hangul fall through to the platform's sans. No serif anywhere. */
--font-body: var(--font-shannon-latin), var(--font-shannon-ext), var(--gpui-font-fallbacks),
ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-display: var(--font-body);
--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
--font-cjk: var(--gpui-font-fallbacks), sans-serif;
--text-mono: var(--gpui-mono-size);
--text-prose: 0.9375rem;
--leading-prose: 1.5334;
/* Hairline + code surfaces routed through vars so the dark subtrees can
re-ink them without touching every rule. Code plates are always the
stage's own deepest field, on either side of the waterline. */
--hairline: rgb(var(--c-ink) / 0.16);
--code-bg: var(--gpui-stage-deep);
--code-fg: var(--gpui-stage-ink);
/* One site container and one vertical rhythm. Every page gutter aligns
with the nav; every hero and section shares a single padding token. */
--container: min(100% - var(--gpui-spacing-page), 76rem);
--hero-pad: clamp(3rem, 6vw, 4.75rem);
--section-pad: clamp(2.75rem, 5vw, 4rem);
/* Radius grammar, as the GPUI client (codewhale-app/docs/DESIGN.md):
6px for rows, chips and small controls; 10px for code and menu
surfaces; 14px for the largest raised sheets; 999px for pills. No other
radius appears in app/styles/ or the Tailwind scale. */
--radius-control: var(--gpui-radius-control);
--radius-surface: var(--gpui-radius-panel);
--radius-sheet: 14px;
--radius-pill: 999px;
/* Motion. The GPUI panel spring is PANEL_SPRING (stiffness 420, damping
42, mass 1; codewhale-app/src/workspace/motion.rs): damping ratio 1.02,
so it never overshoots and reaches 99% at ~340ms. --ease-spring is
that response fitted as one cubic-bezier (max error 1.3%). State
changes (hover, colour, border) use the same curve at --dur-state;
entrances run the full --dur-spring; exits accelerate away. */
--ease-spring: cubic-bezier(0.2, 0.1, 0.16, 1);
--ease-exit: cubic-bezier(0.4, 0, 0.8, 0.4);
--dur-state: 150ms;
--dur-spring: 340ms;
--violet: var(--gpui-selection-dark);
--hairline: var(--gpui-light-border);
--code-bg: var(--gpui-light-sidebar);
--code-fg: var(--gpui-light-text);
color-scheme: light;
}
/* Reduced motion is a still pose, as the GPUI whale under the same setting:
state changes step instead of blending, and every looping or entrance
animation is gated behind `prefers-reduced-motion: no-preference` where
it is declared. */
@media (prefers-reduced-motion: reduce) {
:root {
--dur-state: 0ms;
--dur-spring: 0ms;
}
}
/* ---------- role tokens — GPUI set_theme ---------- */
/*
* One token authority: the GPUI desktop client's `set_theme`
* versioned design artifact (vendor/codewhale-design/tokens.json), generated into
* ./tokens.css as `--gpui-dark-*` / `--gpui-light-*`. Nothing below repeats a
* hex for a role: every role value is a generated token, or the primary at
* the opacity `set_theme` derives (hover 0.9, selection 0.28).
*
* The role layer follows the OS: light by default, dark under
* `prefers-color-scheme: dark` unless the page pins `data-theme="light"`,
* and dark whenever it pins `data-theme="dark"`.
*
* --bg background the reading canvas
* --surface sidebar recessed navigation and chrome
* --panel muted raised inputs, cards, popovers
* --text foreground
* --muted muted_foreground secondary text (AA on --bg and --panel)
* --line border
* --accent primary action, links, whale blue
* --on-accent primary_foreground
* --hover accent hover wash, distinct from --panel
* --selected list_active the current row
* --selection primary @ 0.28 text selection
* --ring primary focus ring
*
* Shoreline (`--shoreline-*`) is the TUI's charcoal theme and is not the
* GPUI palette; `--whale-*`/`--light-*` remain available to anything that
* genuinely wants the terminal palette.
*/
:root {
--bg: var(--gpui-light-bg);
--surface: var(--gpui-light-sidebar);
--panel: var(--gpui-light-panel);
--text: var(--gpui-light-text);
--muted: var(--gpui-light-text-muted);
--line: var(--gpui-light-border);
--accent: var(--gpui-light-primary);
--on-accent: var(--gpui-light-on-primary);
--hover: var(--gpui-light-accent);
--selected: var(--gpui-light-list-active);
--selection: rgb(var(--gpui-light-primary-rgb) / var(--gpui-selection-opacity));
--ring: var(--gpui-light-primary);
}
/* ---------- dark — graphite; the OS-dark and pinned blocks match ---------- */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: var(--gpui-dark-bg);
@@ -229,14 +251,27 @@
--text: var(--gpui-dark-text);
--muted: var(--gpui-dark-text-muted);
--line: var(--gpui-dark-border);
--line-strong: var(--gpui-dark-border-strong);
--accent: var(--gpui-dark-primary);
--accent-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
--on-accent: var(--gpui-dark-on-primary);
--hover: var(--gpui-dark-accent);
--selected: var(--gpui-dark-list-active);
--selection: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
--ring: var(--gpui-dark-primary);
--live: var(--gpui-dark-live);
--success: var(--gpui-dark-live);
--attention: var(--gpui-dark-attention);
--danger: var(--gpui-dark-danger);
--on-danger: var(--gpui-dark-bg);
--diff-add: rgb(36 61 69 / 0.22);
--diff-del: rgb(116 72 84 / 0.15);
--mark-ink: var(--gpui-dark-text);
--glow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 42%, transparent);
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.32);
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
--horizon-edge: var(--gpui-dark-border);
/* The folio names, re-inked in the GPUI charcoal ramp. */
--c-paper: var(--gpui-dark-bg-rgb);
--c-paper-deep: var(--gpui-dark-panel-rgb);
--c-paper-edge: var(--gpui-dark-border-rgb);
@@ -261,22 +296,17 @@
--cyan: var(--gpui-primary-dark);
--ocean-coral: #d08a80;
--signal-gold: #d6c78f;
--mark-ink: var(--gpui-stage-ink);
--docs-accent: var(--gpui-primary-dark);
--docs-button-bg: var(--gpui-stage-muted);
--docs-button-border: var(--gpui-stage-edge);
--docs-button-text: var(--gpui-stage-ink);
--hairline: rgb(var(--c-stage-soft) / 0.2);
--hairline: var(--gpui-dark-border);
--code-bg: var(--gpui-dark-sidebar);
--code-fg: var(--gpui-dark-text);
color-scheme: dark;
}
}
/* The pinned dark scheme repeats the OS-dark block exactly. The stage
subtrees (the water column and the footer seabed) take the same block in
either scheme, so a component never needs to know which side of the
waterline it is on. */
.ocean-column,
.site-footer,
:root[data-theme="dark"] {
--bg: var(--gpui-dark-bg);
--surface: var(--gpui-dark-sidebar);
@@ -284,14 +314,27 @@
--text: var(--gpui-dark-text);
--muted: var(--gpui-dark-text-muted);
--line: var(--gpui-dark-border);
--line-strong: var(--gpui-dark-border-strong);
--accent: var(--gpui-dark-primary);
--accent-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
--on-accent: var(--gpui-dark-on-primary);
--hover: var(--gpui-dark-accent);
--selected: var(--gpui-dark-list-active);
--selection: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
--ring: var(--gpui-dark-primary);
--live: var(--gpui-dark-live);
--success: var(--gpui-dark-live);
--attention: var(--gpui-dark-attention);
--danger: var(--gpui-dark-danger);
--on-danger: var(--gpui-dark-bg);
--diff-add: rgb(36 61 69 / 0.22);
--diff-del: rgb(116 72 84 / 0.15);
--mark-ink: var(--gpui-dark-text);
--glow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 42%, transparent);
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.32);
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
--horizon-edge: var(--gpui-dark-border);
/* The folio names, re-inked in the GPUI charcoal ramp. */
--c-paper: var(--gpui-dark-bg-rgb);
--c-paper-deep: var(--gpui-dark-panel-rgb);
--c-paper-edge: var(--gpui-dark-border-rgb);
@@ -316,11 +359,83 @@
--cyan: var(--gpui-primary-dark);
--ocean-coral: #d08a80;
--signal-gold: #d6c78f;
--mark-ink: var(--gpui-stage-ink);
--docs-accent: var(--gpui-primary-dark);
--docs-button-bg: var(--gpui-stage-muted);
--docs-button-border: var(--gpui-stage-edge);
--docs-button-text: var(--gpui-stage-ink);
--hairline: rgb(var(--c-stage-soft) / 0.2);
--hairline: var(--gpui-dark-border);
--code-bg: var(--gpui-dark-sidebar);
--code-fg: var(--gpui-dark-text);
color-scheme: dark;
}
/* ---------- midnight — the stage below the horizon ---------- */
/* Always the dark set, in both appearances (MEASUREMENTS §5.1): the footer
and any home descent. The ground is the deepest graphite, so raised and
recessed plates step up from it. `.ocean-column` stays until the home
slice retires it. --horizon-edge is deliberately not redeclared: it
follows the page's appearance. */
.midnight,
.site-footer,
.ocean-column {
--bg: var(--gpui-dark-sidebar);
--surface: var(--gpui-dark-bg);
--panel: var(--gpui-dark-panel);
--text: var(--gpui-dark-text);
--muted: var(--gpui-dark-text-muted);
--line: var(--gpui-dark-border);
--line-strong: var(--gpui-dark-border-strong);
--accent: var(--gpui-dark-primary);
--accent-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
--on-accent: var(--gpui-dark-on-primary);
--hover: var(--gpui-dark-accent);
--selected: var(--gpui-dark-list-active);
--selection: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-selection-opacity));
--ring: var(--gpui-dark-primary);
--live: var(--gpui-dark-live);
--success: var(--gpui-dark-live);
--attention: var(--gpui-dark-attention);
--danger: var(--gpui-dark-danger);
--on-danger: var(--gpui-dark-bg);
--diff-add: rgb(36 61 69 / 0.22);
--diff-del: rgb(116 72 84 / 0.15);
--mark-ink: var(--gpui-dark-text);
--glow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 42%, transparent);
--glow-focus: 0 0 14px 2px rgb(var(--gpui-dark-primary-rgb) / 0.32);
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
--c-paper: var(--gpui-dark-sidebar-rgb);
--c-paper-deep: var(--gpui-dark-panel-rgb);
--c-paper-edge: var(--gpui-dark-border-rgb);
--c-ink: var(--gpui-dark-text-rgb);
--c-ink-soft: var(--gpui-dark-text-muted-rgb);
--c-ink-mute: var(--gpui-dark-text-muted-rgb);
--c-indigo: var(--gpui-dark-primary-rgb);
--paper: var(--gpui-stage-deep);
--paper-deep: var(--gpui-stage-muted);
--paper-edge: var(--gpui-stage-edge);
--paper-card: var(--gpui-stage-muted);
--paper-line: var(--gpui-stage-edge);
--paper-line-soft: var(--gpui-stage-edge);
--ink: var(--gpui-stage-ink);
--ink-soft: var(--gpui-stage-ink-soft);
--ink-mute: var(--gpui-stage-ink-dim);
--indigo: var(--gpui-primary-dark);
--indigo-deep: var(--gpui-primary-hover-dark);
--indigo-pale: rgb(var(--c-primary-dark) / 0.14);
--ochre: #d6c78f;
--jade: #9ec7b2;
--cyan: var(--gpui-primary-dark);
--ocean-coral: #d08a80;
--signal-gold: #d6c78f;
--docs-accent: var(--gpui-primary-dark);
--docs-button-bg: var(--gpui-stage-muted);
--docs-button-border: var(--gpui-stage-edge);
--docs-button-text: var(--gpui-stage-ink);
--hairline: var(--gpui-dark-border);
--code-bg: var(--gpui-dark-bg);
--code-fg: var(--gpui-dark-text);
color-scheme: dark;
background: var(--bg);
color: var(--text);
}
+34 -166
View File
@@ -1,197 +1,65 @@
/* ------------------------------------------------------------------ */
/* Motion — entrances, considered hovers, and one-shot settles */
/* Utilities — focus, selection, links, the mobile sheet's motion */
/* ------------------------------------------------------------------ */
/*
* Grammar: the page is complete and static; motion answers a person's action
* (a hover draws a rule, a press stamps a copy). The one authored ambient
* moment is the column breath above. Everything here animates transform
* and/or opacity only and is gated on `no-preference`, so reduced motion is
* the *absence* of motion rather than an override fighting it.
*/
/* The nav-link underline draw, extended to the other mono-voice links. Hover
draws the rule; the transition is the whole effect, so reduced motion keeps
the existing colour change and simply skips the draw. */
.product-start-links a,
.gs-step-link,
.product-surfaces > .product-container > a,
.product-community nav a {
position: relative;
/* ---------- focus + selection ---------- */
/* The one focus treatment on the site: a 2px ring at a 3px offset in the
accent of whatever depth the control sits in (midnight re-declares
--ring). Components may add the bioluminescent halo (--glow-focus) as a
box-shadow; none restyles or removes the ring. */
::selection { background: var(--selection); }
:focus-visible { outline: var(--gpui-focus-width) solid var(--ring); outline-offset: var(--gpui-focus-offset); }
/* ---------- links ---------- */
a { color: inherit; text-decoration: none; }
/* A link in running text: accent ink, a quiet underline that firms on
hover. `.body-link` is the legacy name for the same recipe. */
.link,
a.body-link {
color: var(--accent);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
transition: text-decoration-color var(--dur-state) var(--ease-out);
}
.link:hover,
a.body-link:hover { text-decoration-color: var(--accent); }
.product-start-links a::after,
.gs-step-link::after,
.product-surfaces > .product-container > a::after,
.product-community nav a::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -2px;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform var(--dur-state) var(--ease-spring);
}
@media (prefers-reduced-motion: no-preference) {
.product-start-links a:hover::after,
.gs-step-link:hover::after,
.product-surfaces > .product-container > a:hover::after,
.product-community nav a:hover::after {
transform: scaleX(1);
}
}
/* The primary-button arrow leans into the direction it points. */
.product-button > span[aria-hidden="true"] {
display: inline-block;
transition: transform var(--dur-state) var(--ease-spring);
}
@media (prefers-reduced-motion: no-preference) {
.product-button:hover > span[aria-hidden="true"],
.product-button:focus-visible > span[aria-hidden="true"] {
transform: translateX(2px);
}
}
/* Copy-to-clipboard: the press is a stamp, the confirmation a jade seal —
the wire's merge verb colour, borrowed for one beat. The colour change is
unconditional (it is the confirmation); only the motion is gated. */
.copy-btn[data-copied="true"] {
background: var(--jade);
color: var(--paper);
}
@media (prefers-reduced-motion: no-preference) {
.copy-btn {
transition: background-color var(--dur-state) var(--ease-spring), color var(--dur-state) var(--ease-spring), transform var(--dur-state) var(--ease-spring);
}
.copy-btn:active {
transform: scale(0.95);
}
.copy-btn[data-copied="true"] {
animation: copy-settle var(--dur-spring) var(--ease-spring);
}
}
@keyframes copy-settle {
0% { transform: scale(1); }
35% { transform: scale(0.93); }
70% { transform: scale(1.04); }
100% { transform: scale(1); }
}
/* Mobile menu: the sheet drops a few points while its items settle in
sequence; exit is a short fade. The exit is driven by a delayed unmount in
components/mobile-menu.tsx — with reduced motion the menu mounts and
unmounts instantly, exactly as before. */
/* ---------- the mobile sheet ---------- */
/* The sheet arrives (opacity plus a 4px rise) and leaves faster than it
came. No per-item stagger. Reduced motion mounts and unmounts at once. */
@media (prefers-reduced-motion: no-preference) {
.mm-panel {
animation: mm-in var(--dur-spring) var(--ease-spring) both;
animation: mm-in var(--dur-arrive) var(--ease-out) both;
}
.mm-panel.mm-closing {
animation: mm-out 170ms var(--ease-exit) both;
animation: mm-out var(--dur-exit) var(--ease-in) both;
pointer-events: none;
}
.mm-panel li,
.mm-panel nav > a {
animation: mm-rise var(--dur-spring) var(--ease-spring) both;
}
.mm-panel li:nth-child(1) { animation-delay: 34ms; }
.mm-panel li:nth-child(2) { animation-delay: 58ms; }
.mm-panel li:nth-child(3) { animation-delay: 82ms; }
.mm-panel li:nth-child(4) { animation-delay: 106ms; }
.mm-panel li:nth-child(5) { animation-delay: 130ms; }
.mm-panel li:nth-child(6) { animation-delay: 154ms; }
.mm-panel li:nth-child(7) { animation-delay: 178ms; }
.mm-panel li:nth-child(8) { animation-delay: 202ms; }
.mm-panel nav > a { animation-delay: 226ms; }
}
@keyframes mm-in {
from { opacity: 0; transform: translateY(-8px); }
from { opacity: 0; transform: translateY(-0.25rem); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes mm-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-6px); }
}
@keyframes mm-rise {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* ---------- decorative big CJK in margin ---------- */
.margin-glyph {
font-family: var(--font-cjk);
font-weight: 700;
color: var(--ink);
opacity: 0.04;
font-size: 18rem;
line-height: 0.9;
pointer-events: none;
user-select: none;
position: absolute;
}
/* ---------- focus + selection ---------- */
/* The one focus treatment on the site: a 2px primary ring. No component
restyles it; inputs keep the same ring rather than a glow. */
::selection { background: var(--selection); }
:focus-visible { outline: var(--gpui-focus-width) solid var(--ring); outline-offset: var(--gpui-focus-offset); }
/* The dark stage keeps the light scheme's page but a sidebar-dark field;
there the light primary ring is ~2.8:1 (#245bc7 on #191a1c), under the 3:1
a focus indicator needs, so stage surfaces take the dark scheme's ring. */
.ocean-column,
.site-footer,
.folio-shot,
.portal-quickstart,
.portal-community,
.contribute-dev-loop,
pre.code-block { --ring: var(--gpui-dark-primary); }
/* ---------- link reset ---------- */
a { color: inherit; text-decoration: none; }
a.body-link {
color: var(--ink);
background-image: linear-gradient(var(--indigo), var(--indigo));
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 1px;
transition: background-size var(--dur-state) var(--ease-spring);
}
a.body-link:hover { background-size: 100% 6px; color: var(--ink); }
/* ---------- mobile-only adjustments ---------- */
@media (max-width: 640px) {
/* Big CJK margin glyph already hidden via tailwind's `hidden lg:block`,
but re-assert that nothing hits the viewport edge. */
.margin-glyph { display: none; }
/* Ticker text gets cramped on phones — shrink + tighten gaps */
.ticker-track { gap: 1.5rem; padding-right: 1.5rem; }
to { opacity: 0; transform: translateY(-0.25rem); }
}
/* ---------- responsive chrome ---------- */
@media (max-width: 900px) {
.site-nav-inner { gap: 1rem; }
.site-github-link { display: none; }
.site-footer-main { grid-template-columns: 1fr; gap: 2.5rem; }
.site-footer-meta { grid-template-columns: minmax(0, 1fr); gap: 0.7rem; }
.site-footer-meta > div { min-width: 0; flex-wrap: wrap; }
}
@media (max-width: 700px) {
.site-wordmark { font-size: 1.05rem; }
}
/* Anchor fallback; page-level scroll-mt utilities should be able to override it. */
/* Anchor fallback; page-level scroll-mt utilities can override it. */
:where([id]) { scroll-margin-top: 5rem; }
+4
View File
@@ -265,6 +265,8 @@
--gpui-dark-accent-rgb: 48 49 52;
--gpui-dark-list-active: #37393d;
--gpui-dark-list-active-rgb: 55 57 61;
--gpui-dark-attention: #e8b077;
--gpui-dark-attention-rgb: 232 176 119;
--gpui-light-bg: #faf8f5;
--gpui-light-bg-rgb: 250 248 245;
--gpui-light-text: #28292b;
@@ -285,6 +287,8 @@
--gpui-light-accent-rgb: 232 229 224;
--gpui-light-list-active: #dfdcd6;
--gpui-light-list-active-rgb: 223 220 214;
--gpui-light-attention: #86520d;
--gpui-light-attention-rgb: 134 82 13;
}
/* GPUI design 1.0.0; sha256 a0c23f8a235bbc61e42fb8a60795ae7d9825dbbd1730142acc9ffec9c0abb3af. */
:root {