Files
agency/.pnpm-store/v3/files/84/525cb1765ab883361605870cd553a4832dfb4d860f107e5b002eb718387480d9838a6669f4f2226a2d9bdf91eb1a29008d07502b710377aae1006a4ce4bb36
hanzo-agent 5af6960b87 run project
Run: sess_f37640604ae254cb4d11dc6217f7b721
2026-09-30 00:04:56 +00:00

230 lines
14 KiB
Plaintext

/* Minimal element defaults so specimen cards and kits inherit the brand without
a utility framework. Components carry their own styles inline.
EVERYTHING HERE IS LAYERED, and that is the most important line in the file.
A rule outside a cascade layer beats a rule inside one no matter how specific
the loser is — so an unlayered `a{color:...}` here outranked EVERY Tailwind
text utility on every anchor, on every surface that imports these tokens. A
white-on-white primary button is what that looks like: `text-neutral-950` on
an anchor, silently overridden to --text-primary, invisible on its own white
fill, and reading as a rendering glitch rather than a cascade bug.
The same trap already bit `text-decoration` here once (see the a:hover note
below). It was fixed by deleting that one declaration, which left the identical
defect in `color` untouched. Layering fixes the whole class: these are
DEFAULTS, and a default must lose to anything an app states deliberately.
The rules added for controls, media and touch go one step further and wrap
their selectors in :where(), which has ZERO specificity. @layer decides who
wins BETWEEN layers; :where() decides who wins INSIDE this one. An app that
ships its own reset — `input { … }`, `img { … }` — very often lands in the
same base layer, and these defaults have to lose that collision too. */
@layer base {
*{box-sizing:border-box;border-color:var(--border)}
html{
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
scroll-behavior:smooth;
/* iOS inflates text when a phone is rotated to landscape unless this is
pinned. It is the single most common reason a mobile layout that was
verified in portrait comes apart on its side. */
-webkit-text-size-adjust:100%;
}
/* Each font-family carries the stack as a literal fallback. --font-sans lives in
tokens/fonts.css, which a surface may legitimately import separately; without
the fallback an unresolved var() makes font-family invalid and the UA drops to
its SERIF default — the whole console silently rendered in Times. */
body{margin:0;background:var(--background);color:var(--foreground);font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);font-size:var(--text-base);line-height:var(--leading-base);font-feature-settings:var(--font-feature-settings);overflow-wrap:break-word}
h1,h2,h3,h4{margin:0;font-family:var(--font-display,var(--font-sans,ui-sans-serif,system-ui,sans-serif));letter-spacing:var(--tracking-tight);color:var(--text-primary)}
p{margin:0;text-wrap:pretty}
code,pre,kbd{font-family:var(--font-mono,ui-monospace,SFMono-Regular,monospace)}
/* No `transition` here: the shared interactive rule below states one, and an
`a` selector (0,0,1) would outrank that :where() (0,0,0) and replace the
whole shorthand — anchors would transition colour and nothing else. */
a{color:var(--text-primary);text-decoration:none;text-underline-offset:4px}
/* Underline is the accessible affordance for a link in RUNNING TEXT and a visual
bug everywhere else: nav items, cards and anchor-buttons are all <a> too, so a
blanket `a:hover` underlined every one of them on every surface that imports
these tokens. Scope it to the elements that actually carry prose; a link that
is a component states its own hover. The old rule also re-set `color` to the
value `a` already has — a no-op that only served to outrank a component's own
hover colour. */
:is(p,li,blockquote,dd,dt,td,th,figcaption) a:hover{text-decoration:underline}
/* ——— the smooth ———
Every interactive element in the reference transitions its colour, its fill
and its EDGE. That is most of what separates "classy and smooth" from a
correct set of colours: a hairline that steps from .10 to .16 instantly
reads as a flicker, and over 150ms reads as a response. Granted once, here,
rather than restated on every component. tokens/motion.css already zeroes
all of it under prefers-reduced-motion. */
:where(a,button,[role=button],input,select,textarea,summary,[tabindex]){
transition:color var(--duration-fast) var(--ease-out),
background-color var(--duration-fast) var(--ease-out),
border-color var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out),
opacity var(--duration-fast) var(--ease-out),
transform var(--duration-press) var(--ease-out);
}
/* ——— the press ———
A button that moves under the finger is the cheapest "this is a real
control" signal there is, and its absence is most of why a correct-looking
button still feels like a picture of a button. 2% over 90ms — small enough
that it reads as pressure rather than as animation, and it is the
reference's own value (.auth-signin-btn:active). Inputs are excluded: a
text field does not depress, and scaling one mid-caret is a glitch. */
:where(button,[role=button],a[role=button],summary):active:not(:disabled){
transform:scale(.98);
}
/* ——— controls ———
A bare <input> renders in the UA's face (Arial) at the UA's size with the
UA's border, which puts two typefaces and a foreign blue focus ring inside
Hanzo cards on every surface that has not styled its own fields yet. The
rules below are what a control looks like here. --border-control, NOT
--border: a field's edge is its own duty and its own rung, so a surface can
retune fields without touching every divider on the page. It owes no
contrast ratio — the whole budget goes to --ring, below. */
:where(input,select,textarea,button){font:inherit;color:inherit}
:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),select,textarea){
background:var(--surface-2);
border:1px solid var(--border-control);
/* --radius-md (8px), not --radius-sm (6px). On a 36px control the two are
one step apart and it is the step between "boxy" and "considered";
--radius-sm keeps its job on genuinely small parts — badges, chips, menu
rows — where 8px would look bubbly. The reference draws its composer and
its buttons at 8-10px and nothing at 6px except 22px chips. */
border-radius:var(--radius-md);
padding:0 var(--space-3);
/* A field's ring is drawn INSIDE its own box. The one focus rule below
reads this; nothing else about the indicator changes.
A field lives in containers a button does not — composers, palettes and
panels that round their corners with `overflow:hidden`. An outline at a
POSITIVE offset is painted outside the border box, so any such ancestor
clips it, and the computed style still says `2px solid` while nothing is
drawn: a chat composer measured byte-identical screenshots focused and
unfocused, and a search pill kept only its left and right edges. A
negative offset cannot be clipped by anything, so a field needs to know
nothing about what contains it.
-2 against a 2px ring lays the outline over the outermost 2px of the box,
so it REPLACES the 1px edge for as long as focus is there rather than
drawing a second line a pixel inside the first. */
--ring-offset:-2px;
}
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
/* A field has NO focus rule of its own — see the ring at the bottom of this
layer, which is the one focus indicator for everything.
There used to be one here: `outline:none` plus a brightened edge (.15 ->
.22) plus a soft halo, the composer look. It was removed because it could
not do the job in either of the two ways that matter.
It was never VISIBLE ENOUGH. Composited on --background the brightened edge
measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
of a focus indicator and that this package already gates --ring on. The
budget was documented as being spent entirely on --ring "because that is
what a keyboard user navigates by" — true for a button, and false for a
field for exactly as long as this rule told fields not to use it.
And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
app that states `border` on its own fields overrode it — @hanzo/id does,
unlayered, which beats this layer whatever its specificity, and its focused
fields sat at the resting .15 while both files read as correct. An outline
is not a border: nothing in an app's field styling reaches it, so the ring
paints whether or not the app has opinions about edges. */
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
:where(input,textarea)::placeholder{color:var(--text-disabled)}
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
:where(button,[role=button],summary,label,select){cursor:pointer}
/* ——— media ———
An image, a video or an embed at its intrinsic width is the usual cause of a
390px page that scrolls sideways. */
:where(img,svg,video,canvas,iframe,picture,object){max-width:100%}
:where(img,video){height:auto}
/* A code block scrolls ITSELF rather than widening the document. */
:where(pre){overflow-x:auto}
/* ——— touch ———
Everything a finger aims at clears 44px, and every field renders at 16px so
iOS does not zoom the viewport on focus. Both are keyed on pointer:coarse —
the actual signal — so a desktop mouse still gets compact controls. This is
the whole of "mobile-first without per-app work": an app that never thought
about phones still has hittable buttons and a viewport that stays put. */
@media (pointer:coarse){
:where(button,[role=button],a[role=button],summary,select,textarea,
input:not([type=checkbox]):not([type=radio]):not([type=hidden])){
min-height:var(--tap-target);
}
:where(input,select,textarea){font-size:var(--text-control)}
/* A checkbox, radio or switch must NOT grow to 44px — the box is 16px
because that is what a checkbox looks like. What has to reach 44px is the
area a finger may land in, so the target is expanded with a centred
pseudo-element that changes nothing about layout or paint. WCAG 2.5.8 puts
the floor at 24px; a 16px box misses it, and every one of these ships at
16px. */
:where([role=checkbox],[role=radio],[role=switch],input[type=checkbox],input[type=radio]){position:relative}
:where([role=checkbox],[role=radio],[role=switch],input[type=checkbox],input[type=radio])::after{
content:'';position:absolute;top:50%;left:50%;
width:var(--tap-target);height:var(--tap-target);
transform:translate(-50%,-50%);
}
}
/* THE focus indicator. One rule, every focusable thing, no exceptions — a
button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
check-tokens holds --ring to that and nothing else here may weaken it.
WHERE the ring sits is the one thing that varies, and it varies as a VALUE
the element carries, never as a second rule: `--ring-offset` defaults to 2
and a field sets it to -2 (see the field block above). Two rules is what
0.4.9 removed, and a second one here would be the same defect again.
There were two rules until 0.4.9, and they collided invisibly. Both computed
to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
each side — so the cascade fell through to SOURCE ORDER inside this layer,
this rule was written later, and it overrode the `outline:none` the field
rule stated expressly to prevent it. Every focused input on every consumer
drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
is why it survived review in both files; the defect existed only in their
order. One rule cannot disagree with itself. */
:focus-visible{outline:2px solid var(--ring);outline-offset:var(--ring-offset,2px)}
/* --white-20 is white-on-white in the light theme, so selection reads through
--selection, which BOTH themes define. */
::selection{background:var(--selection);color:var(--text-primary)}
/* ——— the scrollbar ———
The single largest piece of UNSTYLED chrome left on a dark surface: a
default 15px light-grey scrollbar down the side of a #0a0a0a panel is the
one element on the page that did not get the memo, and it is visible on
every scrolling surface in the product. 6px, no track, thumb on the same
hairline ladder as everything else. Both syntaxes ship because they are
disjoint — `scrollbar-*` is Firefox and the `::-webkit-*` pseudos are
Chromium/Safari; neither engine reads the other.
The thumb is a --white-* rung, so it does not invert; the `.light`
restatements sit here rather than in colors.css because a scrollbar is an
element default and this file is where element defaults live. Everything
stays INSIDE the layer — an unlayered rule here would outrank an app's own
utilities, which is the defect check 1c exists to catch. */
:where(html){scrollbar-color:var(--white-15) transparent;scrollbar-width:thin}
.light{scrollbar-color:rgb(0 0 0 / .18) transparent}
:where(*)::-webkit-scrollbar{width:6px;height:6px}
:where(*)::-webkit-scrollbar-track{background:transparent}
:where(*)::-webkit-scrollbar-thumb{background:var(--white-15);border-radius:var(--radius-full)}
:where(*)::-webkit-scrollbar-thumb:hover{background:var(--white-30)}
:where(*)::-webkit-scrollbar-corner{background:transparent}
/* The stepper arrows, and they are a LINUX defect specifically. Chromium on
macOS draws no buttons, so styling the track and thumb looks finished there
and ships a classic GTK scrollbar — a boxed arrow at each end of every
scroller, on both axes — to everyone on Ubuntu. Styling the other pseudos
does not suppress them; only this does. Zero width AND height because the
horizontal and vertical bars each read one of them. */
:where(*)::-webkit-scrollbar-button{display:none;width:0;height:0}
.light :where(*)::-webkit-scrollbar-thumb{background:rgb(0 0 0 / .18)}
.light :where(*)::-webkit-scrollbar-thumb:hover{background:rgb(0 0 0 / .32)}
}