Files
zeekay 21d5a549cb The whale keeps the left, and Blue answers for the foundation too
The clip is the page rather than a picture on it, and the conversation was
sitting in the middle of it — a column of type over the animal it exists to
show. On a wide screen the column is now on the right and Blue swims in the two
thirds nobody is typing over. It moves once, on the first question: before
anyone speaks it sits wider and further in, because the greeting is the only
content on the page and a narrow ribbon at the edge is not an opening; on the
first question it settles to its talking width at the edge. Two concrete
lengths, so it animates rather than jumping. A phone has no two thirds to
spare, so below 60rem nothing changes.

Blue also answers for the foundation now, not only for the ocean — on the front
door that is most of what people ask. The counts come from the surface Blue is
standing on: zoo.ngo counts its own papers, proposals and open models from the
sources, and hands them over. Blue states those numbers and guesses none. They
arrive as three integers in a plausible range or they do not arrive at all,
because anything a URL can carry reaches a prompt.
2026-09-07 17:56:59 -07:00

487 lines
17 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* The design tokens are NOT authored here. `@hanzo/design` is the one place the
colour ladder, type scale, spacing ramp, radii, elevation and motion are
defined, and every surface in the fleet reads that same file. Carrying a local
copy of the semantic block is how two sites drift a shade apart with nobody
editing anything.
Zen comes from `@hanzo/font`, which is where the family is authored. It is
the FIRST rule in the file on purpose: CSS drops an @import that does not
precede every other rule, so a face declared later in a flattened sheet never
loads. */
@import '@hanzo/font/css';
@import '@hanzo/design/tokens/colors.css';
@import '@hanzo/design/tokens/typography.css';
@import '@hanzo/design/tokens/spacing.css';
@import '@hanzo/design/tokens/radius.css';
@import '@hanzo/design/tokens/elevation.css';
@import '@hanzo/design/tokens/motion.css';
@import '@hanzo/design/tokens/z.css';
/* base.css is the only group with ELEMENT selectors (`a`, `h1`, `body`, …), and
they belong in a layer so that every app rule here outranks them whatever its
specificity. The file already opens `@layer base { … }` itself, so it arrives
layered and this import must NOT restate it: webpack renders
`@import … layer(base)` as `@media layer(base){…}`, which is not a media query
any browser can match, so the whole file was dead in the built site. Measured:
`body` inherited the UA's Times New Roman at 16px, `a` kept the UA underline
on all 95 research links, and CSS.getMatchedStylesForNode returned nothing
from base for <body>. */
@import '@hanzo/design/tokens/base.css';
/* border-box must be UNLAYERED: gui's primitives state box-sizing on their own
elements unlayered, so a layered reset loses to them and every gui View falls
back to content-box — a width:100% box then adds padding outside the measure
and overflows its parent. */
* {
box-sizing: border-box;
}
/* The one place a typeface is named on this site, and it is named by REFERENCE.
@hanzo/design reads `--font-sans` and multiplies it into every ramp and into
gui's `--f-family`; @hanzo/font publishes the family under its own name. Bind
one to the other and nothing in any component has to spell a face. Undeclared,
`var(--font-sans, …)` fell through to the generic tail — the faces loaded and
nothing asked for them.
Left as a default rather than forced: `--font-sans` is one of the knobs
@hanzo/appearance writes INLINE on <html>, so a reader who asks for the system
face, a serif or mono outranks this, and asking for the default again removes
the property and lands back here. */
:root {
--font-sans: var(--font-zen-sans), ui-sans-serif, system-ui, sans-serif;
--font-display: var(--font-sans);
--font-mono: var(--font-zen-mono), ui-monospace, SFMono-Regular, monospace;
}
:root {
/* The five hues of the Zoo mark, and the ink and paper they sit on. They live
here rather than in TypeScript because a colour a stylesheet cannot see is a
colour @hanzo/appearance cannot reach: `lib/brand.ts` now names them by
reference for the gui props that need a value. One home, two readers.
The mark's cyan also fills the design system's accent roles, so a reader who
picks an accent moves the send key rather than nothing. The other six are
the foundation's identity and are passed where they are meant — the active
tab, a card's spine — never as "the accent". */
--ink: #000000;
--paper: #f5e8c8;
/* The deep. The third ground, and the one the door is painted in — the lab is
ink on paper, the door is light on this. It was a literal inside `.deep`,
which made it a colour only that one class could reach; floating chrome
needs it too, and a second copy of a ground is how two dark rooms end up a
shade apart. */
--night: #06202c;
--magenta: #ea018e;
--green: #00a652;
--blue: #2e3192;
--cyan: #01acf1;
--yellow: #fcf006;
--red: #ed1c24;
--primary: var(--cyan);
--accent: var(--primary);
/* The ramp knobs are NOT here. `--type-scale` and `--type-ratio` are the
READER's dial, and this site's own register is the install layer that dial
turns from — `lib/look.ts`, applied before first paint. There is one
`--type-scale` and the panel writes it absolutely, so a stylesheet that
sets it has spent the reader's setting on the brand's size: the panel then
shows "M / Default" selected while the page is at 8/7, and pressing the step
that is already lit drops the anchor rung from 16px to 14px.
`--font-sans` below stays, and the difference is not a preference. The face
has a real absent state — the panel's "Default" writes nothing and the
stylesheet answers — so a default there means THIS SITE's face. The type
steps are absolute numbers with no such state. */
/* Display tracking, under the name zoo.ngo already gives it. --tracking-tight
(−0.025em) is right down the middle of the ramp and too loose at the top of
it: at 64px it leaves 1.6px of air between letterforms that should be
touching. −0.035em is −2.24px there. */
--tracking-display: -0.035em;
}
/* ─── type roles ───
Stated on the ELEMENT, so a page says what a thing IS and this file says what
it looks like. It is also the only place that can say it: gui carries a
letterSpacing of its own on every display rung, as an atomic class in a sheet
it injects at render, so a heading drawn by gui answers to the fleet's tracking
and never to this site's.
Colour is inherited rather than named, because this site has two rooms: the
heading is ink on the paper page and white over the video. */
h1,
h2 {
color: inherit;
font-weight: var(--weight-bold);
text-wrap: balance;
}
h1 {
font-size: var(--text-4xl);
line-height: var(--leading-4xl);
letter-spacing: var(--tracking-display);
}
/* A page head has room from `md` that a phone does not, and on the door the
greeting shares the fold with the composer. */
@media (min-width: 48rem) {
h1 {
font-size: var(--text-5xl);
line-height: var(--leading-5xl);
}
}
/* No leading of its own: the ramp pairs --leading-xl with this rung for a
paragraph, and on a two-line card title it opens a gap you can drive through.
Inherited from the page it lands at 1.3, which is what a title wants. */
h2 {
font-size: var(--text-xl);
}
/* A link inside a heading IS the heading; it is a link only because you can
press it. Said here because base.css colours every `a`, and a rule beats
inheritance however specific the rule is not. */
h1 a,
h2 a {
color: inherit;
}
/* The lockup, as one shape: the org at full weight, what it does at light.
Display type, so it takes the display tracking with the headings. */
.wordmark {
font-size: var(--text-2xl);
font-weight: var(--weight-bold);
letter-spacing: var(--tracking-display);
user-select: none;
}
.wordmark span {
font-weight: 300;
}
body {
margin: 0;
color: var(--ink);
background-color: var(--paper);
/* Iridescent wash: the Zoo mark's five hues dialled down to a pastel tint, so
the page shares the logo's family without competing with the buttons. */
background-image:
radial-gradient(at 14% 16%, #fff3b8 0%, transparent 38%),
radial-gradient(at 86% 12%, #ffc6e6 0%, transparent 40%),
radial-gradient(at 92% 62%, #b8dff8 0%, transparent 42%),
radial-gradient(at 28% 82%, #b8e8c8 0%, transparent 44%),
radial-gradient(at 62% 50%, #c4c8ec 0%, transparent 44%),
radial-gradient(at 8% 92%, #ffc6e6 0%, transparent 40%),
linear-gradient(135deg, #fff3d8 0%, #ffd9ec 28%, #d8dcef 55%, #cfeede 80%, #d6ecf8 100%);
background-attachment: fixed;
background-size: cover;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* ─── the night ───
The dark room, as a set of ROLES rather than a place. @hanzo/design names
every job a colour can hold and scopes the light half of them to `.light`,
which this site wears on <html>; anything drawn on the deep is drawn on the
wrong half of that. It showed: @hanzo/appearance's panel sets its labels in
--foreground, resolved #0a0a0a here, so the shared control that explains the
design system was the one thing on the door that could not be read on it.
Stated once and worn by whatever is in the room — the door below, and the
floating chrome in components/Look, which is over the door as often as not.
Custom properties inherit, so a class on the element beats the `.light` on
<html> for its whole subtree without out-specifying anything. Every value is
a rung of the design system's own opacity ladder, so the surfaces inside lift
by the steps the fleet lifts by instead of by numbers chosen here.
The GROUND is not here, and that is the one thing this class does not decide.
gui paints a component's own background as an atomic class it injects at
render, which a rule in this file cannot outrank — so each wearer lays its
own: the door below as a full-bleed fixed layer, the panel as a 320px card.
`--night` is the value both of them ask for, so there is still one dark.
--ink is deliberately absent for the same reason from the other direction:
@hanzo/gui publishes one at `:root.t_light` and a class cannot outrank that
on <html>. It stays the hard edge this site draws every panel with, which is
what it is for. */
.night,
.deep {
color: #ffffff;
--foreground: #ffffff;
--muted-foreground: var(--white-78);
--secondary: var(--white-08);
--surface-card: var(--white-14);
/* A filled accent carries the night as its ink — which is what the door's own
send key already does by hand. */
--primary-foreground: var(--night);
}
/* The deep — the logged-out door at /.
A different room from the lab: the clip is the page rather than a picture on
it, so everything here sits over video and the type is light on dark. The
lab, and every other Zoo surface, stays on the paper ground above. It IS the
night, so it is listed with it above rather than repeating the ground. */
.deep {
position: fixed;
inset: 0;
overflow: hidden;
}
/* Two of these are stacked, and they trade places rather than one of them
changing what it plays — see components/Film.tsx. The dissolve is here
because it is how the page looks; which of the pair is in front is state,
and stays in the component. */
.deep-film {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 700ms ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.deep-film {
transition: none;
}
}
/* Darkened toward the bottom, where the question box and the type sit. */
.deep-veil {
position: absolute;
inset: 0;
background:
linear-gradient(to bottom, rgba(3, 20, 30, 0.35) 0%, rgba(3, 20, 30, 0.15) 35%, rgba(3, 20, 30, 0.8) 100%);
}
.deep-stage {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
color: #fff;
/* The clip behind this is different every few seconds and some frames are
nearly white, so the type carries its own contrast rather than trusting
the veil to provide it. */
text-shadow: 0 1px 14px rgba(0, 20, 30, 0.75), 0 0 3px rgba(0, 20, 30, 0.5);
}
/* Sizes here are RUNGS, never numbers. Every one is a `--text-*` step, so the
reader's own type setting reaches the door as surely as it reaches a heading;
a literal rem would be the one place the knob could not move. The door reads
a rung apart from the page because it is a conversation and not a dense
screen: the field at `xl`, an answer at `lg`, a chip at `sm`, the line under
the field at `xs`. */
.deep-chip {
padding: 0.5rem 0.9rem;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: var(--radius-full);
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(8px);
color: #fff;
font: inherit;
font-size: var(--text-sm);
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.deep-chip:hover {
background: rgba(255, 255, 255, 0.16);
border-color: rgba(255, 255, 255, 0.45);
}
/* The measure is the design system's, not a number: `--container-prose` is the
width prose reads at across the estate, and it is what @hanzo/appearance moves
when a reader asks for a narrower or wider page. */
/* THE WHALE KEEPS THE LEFT. On a wide screen the conversation is a column on
the right and the clip is not covered: Blue swims in the two thirds nobody is
typing over, which is the whole reason the clip is the page. A phone has no
two thirds to spare, so below 60rem the column is the width of the screen and
this rule does not apply.
The column also MOVES once. Before anyone speaks it sits wider and further in,
because the greeting is the page's only content and a narrow ribbon at the
edge is not an opening. On the first question it settles to its talking width
at the edge — one transition on two concrete lengths, so it animates rather
than jumping, and the whale is clear from the first frame either way. */
@media (min-width: 60rem) {
.deep-stage {
--talk: min(38vw, 30rem);
--sit: min(52vw, 40rem);
--edge: clamp(1.5rem, 4vw, 4rem);
}
.deep-middle,
.deep-ask-row {
width: var(--sit);
margin-left: auto;
margin-right: calc((100vw - var(--sit)) / 2);
transition: width 520ms cubic-bezier(0.22, 1, 0.36, 1),
margin-right 520ms cubic-bezier(0.22, 1, 0.36, 1);
}
.talking .deep-middle,
.talking .deep-ask-row {
width: var(--talk);
margin-right: var(--edge);
}
/* The thread and the field are the column, so they stop asking for a measure
of their own — the column is the measure. */
.deep-thread,
.deep-ask {
max-width: none;
}
}
/* Between the bar and the ask, and it SHRINKS. A flex child sizes to its
content by default, so on a short window — a phone in landscape, or this page
in the height someone else's hero gives it — the greeting and the openers
spilled out of both ends and printed over the bar and over the composer.
Scrolling inside is what a room does when it is smaller than what is in it. */
.deep-middle {
min-height: 0;
overflow-y: auto;
}
/* A SHORT ROOM. The door is normally a whole viewport; it is also a phone in
landscape, and the height someone else's hero gives it. Under about 34rem the
greeting has to give way to the thing a reader came to do — so the type comes
down, the spacing closes, and the third opener stands aside rather than
sitting behind the ask. Two questions are still an invitation. */
@media (max-height: 34rem) {
.deep-middle h1 {
font-size: var(--text-2xl);
line-height: 1.15;
}
.deep-middle {
gap: 0.5rem;
}
.deep-chip:nth-of-type(n + 3) {
display: none;
}
}
.deep-thread {
width: 100%;
max-width: var(--container-prose);
max-height: 58vh;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-right: 0.25rem;
}
/* A turn takes ITS OWN SIDE. Blue's had neither an align-self nor a width, so
in a column flexbox it stretched the full thread while the reader's hugged
the right — one voice as a wall of text, the other as a bubble, which does
not read as two people talking. Both are pinned now, and both stop short of
the far edge so the shape of the exchange is visible at a glance. */
.deep-said {
align-self: flex-start;
max-width: 85%;
padding: 0.75rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 14px;
background: rgba(4, 22, 32, 0.62);
backdrop-filter: blur(10px);
font-size: var(--text-lg);
line-height: 1.55;
white-space: pre-wrap;
}
.deep-mine {
align-self: flex-end;
background: rgba(1, 172, 241, 0.28);
border-color: rgba(1, 172, 241, 0.5);
}
.deep-wrong {
border-color: rgba(237, 28, 36, 0.55);
background: rgba(60, 10, 14, 0.6);
}
.deep-ask {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
max-width: var(--container-prose);
padding: 0.45rem 0.45rem 0.45rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: var(--radius-full);
background: rgba(4, 22, 32, 0.7);
backdrop-filter: blur(14px);
}
.deep-ask input {
flex: 1;
min-width: 0;
border: 0;
outline: none;
background: none;
color: #fff;
font: inherit;
/* After the shorthand, which resets size along with everything else. The field
is the page's one action and it is the rung that clears iOS Safari's
zoom-on-focus (anything under 16px buys it). */
font-size: var(--text-xl);
padding: 0.55rem 0;
}
.deep-ask input::placeholder {
color: rgba(255, 255, 255, 0.45);
}
.deep-ask button {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 38px;
height: 38px;
border: 0;
border-radius: var(--radius-full);
background: var(--primary);
color: #04202c;
font-size: var(--text-xl);
font-weight: 700;
line-height: 1;
cursor: pointer;
}
.deep-ask button:disabled {
opacity: 0.4;
cursor: default;
}
/* The speaker sits quieter than the send button: it is a setting, not the
action. It brightens when it is on, so the state reads at a glance and not
only from the icon. */
.deep-ask .deep-voice {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.65);
transition: background 0.15s ease, color 0.15s ease;
}
.deep-ask .deep-voice:hover {
background: rgba(255, 255, 255, 0.18);
color: #fff;
}
.deep-ask .deep-voice[aria-pressed='true'] {
background: rgba(255, 255, 255, 0.92);
color: #04202c;
}