Files
zeekayandHanzo Dev 8530912979 The store's own .btn rule was doing nothing, so it is gone
`.btn` is not a rule @hanzo/ui ships — it is a stable class HANDLE it stamps on
a control alongside the @hanzo/gui atomics that actually paint it, so a host can
hook buttons from CSS. This app hooked it, to refuse the user-agent underline on
the five `<Button asChild>` controls that render as anchors.

It was refusing nothing. Measured on the built site: 606 `a.btn` elements, and
deleting the rule at runtime leaves every one of them computing
`text-decoration-line: none` — ui's own atomics already set it. And there is no
first-paint window where it could have mattered either: the store renders its
buttons on the client, so `.btn` does not appear in the exported HTML at all.

That left a store-owned meaning sitting on ui's handle for no benefit, which is
the thing to avoid — one class name, one owner. The store now defines nothing
for the handles and hooks none of them, so `btn` joins the other nine in
gui-css-check.json.

Recorded there, and worth fixing upstream: @hanzo/gui 8.0.2's css-check ships a
DEFAULT_ALLOW naming `hanzo-button` / `hanzo-badge` — the handles retired in
'adopt @hanzo/ui 8.0.44 class handles'. The defaults have not caught up, which
is the only reason this repo carries a gui-css-check.json at all. When a gui
release allows btn/badge by default, delete the file.

Also documents why this app imports @hanzo/ui/theme.css and not styles.css:
styles.css ends with a bare unscoped `:root{--background:var(--t189)}` that is
near-white, beaten only by a block scoped to gui's own `t_dark` class — which
next-themes never writes. Import it last and the dark ground goes white with
every check still green. theme.css sets --background to #0a0a0a directly.
Asserted with numbers rather than trusted: dark is rgb(20,20,20) ground on
rgb(255,255,255) ink at 18.42:1, light is rgb(247,247,247) on rgb(5,5,5) at
19.02:1 — the ground flips and both clear AAA.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 18:31:06 -07:00

357 lines
10 KiB
CSS

/* The design tokens are NOT authored here. `@hanzo/ui/theme.css` is the one
place the Hanzo colour ladder, radii, type face and elevation are defined, and
every Hanzo surface reads that same file — a local copy of the semantic block
is exactly how two surfaces drift a shade apart without anyone editing
anything. The values `@hanzo/gui` resolves at runtime come from the same
source, so CSS and components cannot disagree.
The faces themselves are a SEPARATE import, and it is not optional. theme.css
only NAMES the fonts (`--font-sans: "Geist", …`); @hanzo/design owns the
@font-face rules and ships the woff2 files beside them, so the urls resolve
from inside node_modules with no config. Omit this line and nothing fails —
the build is green, gui-css-check is green, and every page quietly renders in
the `ui-sans-serif, system-ui` fallback. That is why the e2e pass asserts
`document.fonts.check('16px Geist')` and not just a class name.
Self-hosted on purpose: the store makes no third-party request for type. */
@import '@hanzo/design/tokens/fonts.css';
@import '@hanzo/ui/theme.css';
html,
body {
margin: 0;
padding: 0;
background: var(--background);
color: var(--foreground);
-webkit-font-smoothing: antialiased;
/* A store is one column on a phone: nothing may scroll the body sideways. */
overflow-x: hidden;
}
/* ── Page chrome ──────────────────────────────────────────────────────────────
Document-level layout — the gutter, the sticky bar, the card grid. This is
the layer @hanzo/gui deliberately does not cover: its stacks are flexbox on
every platform, and a responsive column grid is a CSS-only idea. Everything
INSIDE a card is composed from @hanzo/ui components and gui style props;
everything that positions the page is here. Two concerns, two vocabularies,
no overlap. */
.page {
min-height: 100vh;
background: var(--background);
}
.container {
max-width: 80rem;
margin: 0 auto;
padding-inline: 1rem;
}
@media (min-width: 640px) {
.container {
padding-inline: 1.5rem;
}
}
@media (min-width: 1024px) {
.container {
padding-inline: 2rem;
}
}
.topbar {
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid var(--border);
background: color-mix(in oklab, var(--card) 70%, transparent);
backdrop-filter: blur(12px);
}
.bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-block: 1rem;
}
.brand {
display: flex;
align-items: center;
gap: 1rem;
color: var(--foreground);
}
/* The mark is a fixed size, not a flexible one. As a plain flex child it took
its share of the squeeze when the bar ran out of room: 48px on a desktop,
17px at 390px, and 0 — gone entirely — at 320px. The title beside it is the
part that gives. */
.brand > :first-child {
flex: none;
}
/* …and "gives" has to be said out loud, because the words are a @hanzo/gui box
and gui's boxes are React Native boxes: there `flex-shrink` defaults to 0,
not to the web's 1. So the title block held its full 184px, overflowed the
.brand it lives in, and at 390 its right edge landed at 264px while the
Connect Wallet button's left edge landed at 257px — the button is opaque, so
it painted over the tail of the word. `min-width: 0` is the other half of the
fix: a flex item will not shrink past its content's intrinsic width without
it, so shrink alone would have changed nothing. */
.brand > :not(:first-child) {
min-width: 0;
flex-shrink: 1;
}
.back {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
color: var(--muted-foreground);
text-decoration: none;
}
.back:hover {
color: var(--foreground);
}
.main {
padding-block: 3rem;
}
/* One column on a phone, two from tablet, three on a desktop. */
.cards {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
margin-top: 1.5rem;
}
@media (min-width: 768px) {
.cards {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.cards {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.app-icon {
width: 4rem;
height: 4rem;
flex-shrink: 0;
border-radius: var(--radius);
object-fit: cover;
}
/* The app page shows the same mark, larger. One rule, one modifier — a second
`.hero-icon` block would be the same icon described twice. */
.app-icon.lg {
width: 6rem;
height: 6rem;
}
/* No artwork: the initial, in the icon's own box. */
.app-icon.fallback {
display: flex;
align-items: center;
justify-content: center;
background: var(--secondary);
color: var(--secondary-foreground);
font-size: 2.25rem;
font-weight: 700;
}
/* ── App page ─────────────────────────────────────────────────────────────────
One column on a phone; the record on the left and the install/metadata rail
on the right from a laptop up. */
.detail {
display: grid;
grid-template-columns: 1fr;
align-items: start;
gap: 2rem;
}
@media (min-width: 1024px) {
.detail {
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
}
}
.shot {
width: 100%;
border: 1px solid var(--border);
border-radius: var(--radius);
}
/* Commands and config are prose the theme already typesets — theme.css binds
`code`/`pre` to Geist Mono, so this only gives the block its box. */
.code {
margin: 0;
padding: 0.75rem;
border-radius: var(--radius);
background: var(--muted);
color: var(--foreground);
font-size: 0.8125rem;
line-height: 1.6;
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.site-footer {
margin-top: 5rem;
padding-block: 3rem;
border-top: 1px solid var(--border);
text-align: center;
}
.links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
font-size: 0.875rem;
}
.links a {
color: var(--muted-foreground);
text-decoration: none;
}
.links a:hover {
color: var(--foreground);
}
.copyright {
margin: 1.5rem 0 0;
font-size: 0.8125rem;
color: var(--muted-foreground);
}
/* ── Documents ────────────────────────────────────────────────────────────────
Guidelines / Terms / Privacy are prose, not product chrome: long-form legal
copy whose structure IS its markup. So they are written as semantic HTML and
typeset once, here, instead of carrying a utility class on every heading and
list item. One scope, one place to retune the reading experience, and the
pages stay diffable against the legal text they mirror. */
.doc {
max-width: 56rem;
margin: 0 auto;
padding: 3rem 1rem;
font-size: 0.9375rem;
line-height: 1.7;
color: var(--foreground);
}
.doc h1 {
margin: 0 0 1rem;
font-size: 2.25rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.doc h2 {
margin: 2.5rem 0 1rem;
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.doc h3 {
margin: 1.75rem 0 0.5rem;
font-size: 1.125rem;
font-weight: 600;
}
.doc p {
margin: 0 0 1rem;
}
.doc ul {
margin: 0 0 1rem;
padding-left: 1.5rem;
list-style: disc;
}
.doc ul.plain {
padding-left: 0;
list-style: none;
}
.doc li {
margin-bottom: 0.5rem;
}
.doc a {
color: var(--foreground);
text-decoration: underline;
text-underline-offset: 3px;
}
.doc .muted {
color: var(--muted-foreground);
}
/* A callout is prose too — a boxed digest of the section it opens, typeset by
the document rather than by importing a component vocabulary mid-text. */
.doc .callout {
display: block;
margin: 0 0 2rem;
padding: 1.5rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--card);
}
.doc .callout h2 {
margin-top: 0;
}
.doc .callout ul:last-child,
.doc .callout p:last-child {
margin-bottom: 0;
}
/* The closing affirmation on every document — smaller, quieter, ruled off. */
.doc .coda {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--border);
font-size: 0.875rem;
color: var(--muted-foreground);
}
/* ── RainbowKit ───────────────────────────────────────────────────────────────
The wallet modal is a third-party surface with its own variable namespace.
Rather than restating a palette, each of its variables is bound to the Hanzo
token that already means the same thing, so the modal moves when the theme
moves. This is the only place in the app that names --rk-*. */
[data-rk] {
--rk-colors-accentColor: var(--primary) !important;
--rk-colors-accentColorForeground: var(--primary-foreground) !important;
--rk-colors-modalBackground: var(--popover) !important;
--rk-colors-modalBorder: var(--border) !important;
--rk-colors-modalText: var(--popover-foreground) !important;
--rk-colors-modalTextSecondary: var(--muted-foreground) !important;
--rk-colors-modalTextDim: var(--muted-foreground) !important;
--rk-colors-generalBorder: var(--border) !important;
--rk-colors-generalBorderDim: var(--border) !important;
--rk-colors-actionButtonBorder: var(--border) !important;
--rk-colors-actionButtonSecondaryBackground: var(--secondary) !important;
--rk-colors-connectButtonBackground: var(--card) !important;
--rk-colors-connectButtonText: var(--card-foreground) !important;
--rk-colors-connectButtonInnerBackground: var(--secondary) !important;
--rk-colors-menuItemBackground: var(--secondary) !important;
--rk-colors-closeButton: var(--muted-foreground) !important;
--rk-colors-closeButtonBackground: var(--secondary) !important;
--rk-radii-modal: var(--radius) !important;
--rk-radii-connectButton: var(--radius) !important;
--rk-radii-actionButton: var(--radius) !important;
--rk-radii-menuButton: var(--radius) !important;
}