mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
Every credential this portal takes was masked with no way to unmask. That is
the most common cause of a sign-in failure that looks like a wrong password,
and it is worst where typing is least reliable -- a phone keyboard, one glyph
at a time, with autocorrect and a shifted layout in the way. On SIGNUP it is
worse still: the account is created with whatever was actually typed, so a
typo behind the dots locks you out of an account you believe you just made.
ONE PasswordField, used by both forms. Login and signup had separate
hand-rolled <label>+<input> pairs; adding a toggle to each would have been two
answers to one question. Both now render the same primitive, so the field is
also the one place autoComplete is stated (current-password vs new-password --
getting that wrong is how a manager offers to "update" a saved credential
during a fresh signup).
Details that are the whole feature:
- type="button". A bare <button> in a <form> defaults to submit, so the
reveal would have submitted the form.
- The button is OUTSIDE the <label>. A <button> inside one is activated
twice -- as itself, and again by the label forwarding to its control -- so
the toggle would fight itself. htmlFor keeps click-the-text-to-focus.
- 44x44, the touch floor this stylesheet already holds everywhere else. An
icon button only as big as its 20px glyph is a miss on a phone, which is
the device this is for.
- The input gives back 58px of right padding, so a long password never runs
under the eye.
- aria-pressed carries the state and the colour follows it, not :hover --
on touch there is no hover, and "is my password on screen" must not
depend on where a pointer is.
- Icons live in ui/icons.tsx with the provider marks, inline and
currentColor-driven. No icon dependency on the credential path.
Measured in a real browser on the built bundle, /login and /signup, at 390 /
768 / 1440 with the viewport actually resized between readings: button 44x44,
input 44 high, button inside the input box, no horizontal overflow, in all six.
Toggle across frames: password -> text -> password, aria-pressed false -> true
-> false. Suite 170/170, typecheck clean.
Default is masked. Nothing about the resting page changes.
Co-authored-by: Hanzo Dev <[email protected]>
611 lines
26 KiB
CSS
611 lines
26 KiB
CSS
/* Hanzo ID — the styling layer for hanzo.id / lux.id / zoo.id / pars.id.
|
|
*
|
|
* TOKENS COME FROM @hanzo/design. Not one colour, radius or type size is
|
|
* invented here; every value below resolves to a design token, so a token change
|
|
* lands on all four brand portals AND on pay.hanzo.ai — the other half of the
|
|
* same sign-in-then-pay flow — at once.
|
|
*
|
|
* ARCHITECTURE RULE, learned the hard way. A component's surface must never
|
|
* depend on WHERE it is mounted. This file used to paint controls with the
|
|
* descendant selectors `form input {…}` and `.hanzo-id-btn, form button {…}`,
|
|
* so any control that escaped a <form> ancestor fell out of the stylesheet and
|
|
* rendered as raw UA chrome: the device-approval screen (2 inputs, 0 forms)
|
|
* showed a 31px beveled browser input next to correctly-styled 44px siblings.
|
|
* That is the same class of defect as a distributed component shipping utility
|
|
* class names with no CSS behind them. Every rule below is keyed to a CLASS the
|
|
* component itself carries — `.hanzo-id-input`, `.hanzo-id-btn`,
|
|
* `.hanzo-id-field`, `.hanzo-id-form` — and there are no element-descendant
|
|
* selectors for surface anywhere in this file.
|
|
*/
|
|
|
|
/* ONE import, the whole token layer. This file used to cherry-pick four of the
|
|
* nine token groups, which meant z, elevation, spacing, fonts and the element
|
|
* defaults simply did not exist here — and an absent group is invisible: an
|
|
* unresolved var() paints nothing and reports no error. @hanzo/iam's account
|
|
* menu alone reaches for --z-popover, --shadow-floating and --space-1..3, none
|
|
* of which the four-group subset carried.
|
|
*
|
|
* The reason for cherry-picking is gone: as of @hanzo/design 0.3.0 Geist is
|
|
* SELF-HOSTED inside the package (two variable woff2, SIL OFL-1.1), so
|
|
* tokens/fonts.css no longer makes a request to fonts.googleapis.com and the
|
|
* sign-in path can take the typeface along with the colours. */
|
|
@import '@hanzo/design/styles.css';
|
|
|
|
:root {
|
|
color-scheme: dark;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
html, body, #root { height: 100%; margin: 0; }
|
|
/* 100dvh, with 100vh left underneath it as the fallback for anything that does
|
|
not know the unit. On a phone `vh` resolves against the LARGEST viewport —
|
|
the one with the URL bar retracted — so a bar that is actually on screen
|
|
makes a "full height" page taller than the space it has, and a login page
|
|
with one card on it acquires a scrollbar and sits low in the window. `dvh`
|
|
is the height that is really there. */
|
|
body {
|
|
background: var(--background);
|
|
color: var(--foreground);
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
font-size: var(--text-base);
|
|
}
|
|
|
|
/* NO focus rule here any more — tokens/base.css ships
|
|
`:focus-visible{outline:2px solid var(--ring);outline-offset:2px}` to every
|
|
consumer, and as of 0.3.0 --ring is var(--neutral-500), which measures 4.43:1
|
|
on --background (WCAG 2.4.13 wants 3:1). The local override that painted the
|
|
ring --primary existed only because --ring was #333333 at 1.66:1; that was a
|
|
finding against @hanzo/design, it has been fixed there, so the workaround
|
|
goes. One focus indicator, defined once, in the design system. */
|
|
|
|
/* ── Page shell ────────────────────────────────────────────────────── */
|
|
|
|
.hanzo-id-page {
|
|
flex: 1;
|
|
/* #root is not a flex container, so `flex: 1` alone never stretches this to
|
|
the viewport — pin a min height so `main`'s justify-content:center has room
|
|
to vertically center the auth card (box-sizing:border-box folds in padding).
|
|
dvh for the reason given on `body`; the vh line is the fallback. */
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
/* index.html declares `viewport-fit=cover`, which puts the page UNDER the
|
|
notch, the rounded corners and the home indicator. Declaring cover without
|
|
consuming the insets is strictly worse than not declaring it, and nothing
|
|
in this stylesheet consumed them: `max()` keeps the 24px gutter everywhere
|
|
it is already enough and only grows it where the hardware intrudes, so
|
|
nothing moves on a device with no insets (env() is 0px there). */
|
|
padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right))
|
|
max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
|
|
}
|
|
|
|
.hanzo-id-brand-header { padding: 16px 0 32px; }
|
|
|
|
/* The home link wraps a 32px mark but measured 32x18 — an inline <a> takes its
|
|
box from the LINE BOX of its own font, not from a replaced child, so the tap
|
|
target was SMALLER than the logo inside it. inline-flex gives the anchor the
|
|
mark's real box; the padding/negative-margin pair then grows the hit area to
|
|
the 44px floor while leaving the mark exactly where it was — the padding
|
|
offsets the margin, so the logo's painted position and the header's height
|
|
are both unchanged, and only the invisible target bleeds into the gutter. */
|
|
.hanzo-id-brand-header a {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
box-sizing: content-box;
|
|
min-width: 32px;
|
|
min-height: 32px;
|
|
padding: 6px;
|
|
margin: -6px;
|
|
}
|
|
|
|
/* The lockup is 32px tall for EVERY brand, and it has to be said here.
|
|
BrandHeader writes `height={32}`, but that is a presentational attribute and
|
|
@hanzo/design's `:where(img,video){height:auto}` overrides it — so the logo
|
|
was sized by whatever the brand package happened to ship, not by this page.
|
|
The two assets differ in exactly the way that hides it: @hanzo/brand's SVG
|
|
carries a viewBox and no width/height, so it has no intrinsic size and landed
|
|
near the intended 32; @luxfi/brand's declares 1024x1024, so it took the full
|
|
column and rendered a 342px mark over lux.id's sign-in form. Same markup,
|
|
same CSS, opposite results, and only the brand nobody was looking at broke.
|
|
Pinning the height and letting width follow makes the header's own
|
|
declaration true again whatever a brand ships. */
|
|
.hanzo-id-brand-header img { height: 32px; width: auto; }
|
|
|
|
/* Text fallback when a brand ships no logo asset (see BrandHeader). */
|
|
.hanzo-id-wordmark {
|
|
font-size: var(--text-xl);
|
|
font-weight: var(--weight-bold);
|
|
letter-spacing: var(--tracking-tight);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.hanzo-id-page main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 24px;
|
|
}
|
|
|
|
/* The type scale is the design scale, five rungs: xs / sm / base / xl / 2xl.
|
|
`h1` at --text-2xl is the SAME 24px as pay.hanzo.ai's `text-2xl` headings, so
|
|
a heading does not change size when the flow crosses between the two. */
|
|
.hanzo-id-page h1 {
|
|
margin: 0;
|
|
font-size: var(--text-2xl);
|
|
line-height: var(--leading-2xl);
|
|
letter-spacing: var(--tracking-tight);
|
|
}
|
|
.hanzo-id-page h2 { margin: 0; font-size: var(--text-xl); line-height: var(--leading-xl); }
|
|
.hanzo-id-page .lede { color: var(--muted-foreground); margin: 0; font-size: var(--text-base); }
|
|
|
|
/* ── Field ─────────────────────────────────────────────────────────── */
|
|
|
|
.hanzo-id-form { display: flex; flex-direction: column; gap: 16px; }
|
|
|
|
.hanzo-id-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
font-size: var(--text-sm);
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.hanzo-id-input {
|
|
/* `font: inherit` first: a bare <input>/<button> otherwise renders in the UA
|
|
face (Arial), which put two typefaces inside one 432px card — including on
|
|
the primary CTA. */
|
|
font: inherit;
|
|
font-size: var(--text-base);
|
|
background: var(--white-05);
|
|
color: var(--foreground);
|
|
/* A control's resting edge is --border-control. 0.4.2 cut it back to the alpha
|
|
ladder (.15) because a boundary that clears 3:1 on a near-black page IS a
|
|
mid-grey box, and a form of them reads as a wireframe. The contrast budget
|
|
went to --ring instead — the focus indicator is what a keyboard user
|
|
navigates by, it is the only boundary still held at 3:1, and unlike a
|
|
resting edge it is worth the loudness because it is transient. */
|
|
border: 1px solid var(--border-control);
|
|
/* 8px. 0.4.2 moved every control in the system to --radius-md, and tokens/
|
|
base.css draws a bare field at it; --radius-sm (6px) keeps its job on
|
|
genuinely small parts — badges, chips, menu rows — where 8px looks bubbly. */
|
|
border-radius: var(--radius-md);
|
|
padding: 0 14px;
|
|
min-height: 44px; /* the touch-target floor */
|
|
width: 100%;
|
|
}
|
|
.hanzo-id-input::placeholder { color: var(--text-disabled); }
|
|
.hanzo-id-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
/* Password reveal. The button sits INSIDE the field's box rather than beside it,
|
|
so adding it costs no layout: the row is still one 44px control on one line,
|
|
at every width, and nothing below it moves. */
|
|
.hanzo-id-reveal { position: relative; display: flex; }
|
|
/* Reserve the button's width so a long password never runs under the eye. 14px
|
|
resting pad + 44px target = 58px, which is what the input gives back. */
|
|
.hanzo-id-reveal .hanzo-id-input { padding-right: 58px; }
|
|
.hanzo-id-revealbtn {
|
|
font: inherit;
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
/* 44x44 — the same touch floor every other control here holds, and the reason
|
|
the button is sized rather than just padded: an icon button that is only as
|
|
big as its 20px glyph is a miss on a phone, which is the device this whole
|
|
feature is for. */
|
|
width: 44px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
/* No box of its own: a second bordered surface inside the field would read as
|
|
a nested control. The glyph IS the affordance. */
|
|
background: none;
|
|
border: none;
|
|
border-radius: var(--radius-md);
|
|
padding: 0;
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
transition: color 0.15s ease;
|
|
}
|
|
.hanzo-id-revealbtn:hover { color: var(--foreground); }
|
|
/* Pressed = the password is currently visible. Worth a permanent, non-hover
|
|
signal: the strongest cue that a credential is on screen should not depend on
|
|
where the pointer is, and on touch there is no hover at all. */
|
|
.hanzo-id-revealbtn[aria-pressed='true'] { color: var(--foreground); }
|
|
|
|
/* iOS Safari ZOOMS the viewport when a focused control's text is under 16px,
|
|
and it does not zoom back out — the user is left on a magnified, sideways-
|
|
scrolling sign-in page mid-credential. This rule used to be a COMMENT on
|
|
font-size above claiming `--text-base` prevented that; --text-base is
|
|
0.875rem = 14px, so the protection was never in effect on either credential
|
|
field. The type scale has no 16px rung and should not grow one — 16 is not a
|
|
design value here, it is the threshold in Safari's own zoom heuristic — so it
|
|
is written as the literal it is, and scoped to touch-primary pointers so the
|
|
desktop type ramp is untouched. */
|
|
@media (pointer: coarse) {
|
|
.hanzo-id-input { font-size: 16px; }
|
|
}
|
|
|
|
/* Checkbox: the UA control, sized up from its 13px intrinsic box and tinted with
|
|
the brand. Its touch target is the whole <label> row, which is why it is the
|
|
one control here that is not 44px itself. */
|
|
.hanzo-id-check {
|
|
font: inherit;
|
|
accent-color: var(--primary);
|
|
width: 18px;
|
|
height: 18px;
|
|
min-height: 0;
|
|
margin-top: 2px;
|
|
flex: none;
|
|
}
|
|
|
|
/* ── Button — ONE primitive, two modifiers ─────────────────────────── */
|
|
/* `.hanzo-id-btn` is filled (the primary action). `.ghost` is the secondary
|
|
surface — social sign-in, org rows, Skip/Back. `.row` spreads content for a
|
|
list row. There is no `.primary`: the base IS primary, so there is one and
|
|
only one way to write the default button. */
|
|
|
|
.hanzo-id-btn {
|
|
font: inherit;
|
|
font-size: var(--text-base);
|
|
font-weight: var(--weight-semibold);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
background: var(--primary);
|
|
color: var(--primary-foreground);
|
|
border: 1px solid transparent;
|
|
/* Same 8px as the field it sits under. Two stacked controls in one 432px card
|
|
cannot round differently, and the system draws buttons at 8-10px too. */
|
|
border-radius: var(--radius-md);
|
|
padding: 10px 16px;
|
|
min-height: 44px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
text-align: center;
|
|
transition: background var(--duration-fast) var(--ease-out),
|
|
border-color var(--duration-fast) var(--ease-out);
|
|
}
|
|
.hanzo-id-btn:disabled,
|
|
.hanzo-id-btn[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; }
|
|
.hanzo-id-btn svg { flex: none; }
|
|
|
|
/* The filled button had no hover at all. It has declared `transition:
|
|
background` since it was written and the system has shipped --primary-hover
|
|
the whole time, so the most-clicked control on the portal — Sign in, Continue,
|
|
Create account — was animating a property nothing ever changed. The ghost
|
|
variant below got its hover and this one was simply missed.
|
|
|
|
`:not(.ghost)` rather than relying on order: this selector and the ghost
|
|
hover both compute to (0,4,0), so with a bare `.hanzo-id-btn:hover` the two
|
|
would tie and source order would decide which surface a ghost button lifts
|
|
to. Saying which buttons are meant makes them unable to collide. */
|
|
.hanzo-id-btn:not(.ghost):hover:not(:disabled):not([aria-disabled='true']) {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
.hanzo-id-btn.ghost {
|
|
background: var(--white-05);
|
|
color: var(--foreground);
|
|
border-color: var(--border-strong);
|
|
font-weight: var(--weight-medium);
|
|
}
|
|
/* Hover is a SURFACE lift, not a brighter edge. This used to also set
|
|
`border-color: var(--foreground)` — #ededed, 17.9:1 on --background — which
|
|
painted a near-solid-white 1px wireframe around the two most-hovered controls
|
|
on the page (Continue with GitHub / Continue with Google). Two problems with
|
|
moving the edge on hover, and the second is the one that matters:
|
|
|
|
1. It read as a wireframe, not as a refined surface. The house hairline is a
|
|
low-alpha step (--white-10/-15), ~4x quieter than #ededed.
|
|
2. Brightening an edge is THEME-HOSTILE. Every rung brighter than
|
|
--neutral-500 is worse on white: --neutral-400 measures 8.3:1 on black but
|
|
only 2.52:1 on white, which would fail the same WCAG 1.4.11 floor the
|
|
resting border is documented to hold in BOTH themes. There is no token
|
|
that gets brighter on dark and darker on light, so the edge must not
|
|
encode state at all.
|
|
|
|
So the border stays the constant control boundary (--border-strong, 4.43:1,
|
|
both themes) and the background carries the state — the lift the `transition`
|
|
on .hanzo-id-btn was already animating. */
|
|
.hanzo-id-btn.ghost:hover:not(:disabled) { background: var(--white-10); }
|
|
|
|
.hanzo-id-btn.row { justify-content: space-between; text-align: left; width: 100%; }
|
|
|
|
.hanzo-id-cta-row { display: flex; gap: 12px; }
|
|
.hanzo-id-cta-row .hanzo-id-btn { flex: 1; }
|
|
|
|
.hanzo-id-linkbtn {
|
|
font: inherit;
|
|
background: none;
|
|
border: 0;
|
|
color: var(--text-primary);
|
|
font-size: var(--text-sm);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
padding: 4px 0;
|
|
text-decoration: underline;
|
|
text-underline-offset: 4px;
|
|
}
|
|
|
|
.hanzo-id-footer-links { color: var(--muted-foreground); font-size: var(--text-sm); }
|
|
/* "Forgot password?" / "Create account" / "Sign in" / "Back to sign in" are the
|
|
only route changes on these pages that are not buttons, and they measured 17px
|
|
tall — the line box of their own text. Vertical padding on an INLINE box is
|
|
hit-tested but does not enter line-box height, so this buys the 44px target
|
|
with zero layout movement and no change to the sentences they sit inside.
|
|
(17 + 14 + 14 = 45.) The 14px bleed stays inside `main`'s 24px gap, so no two
|
|
targets overlap. Horizontal size already clears 44px on every one of them. */
|
|
.hanzo-id-footer-links a {
|
|
color: var(--text-primary);
|
|
padding: 14px 0;
|
|
margin: -14px 0;
|
|
}
|
|
|
|
/* A2P SMS consent disclosure (shown on phone/SMS surfaces). */
|
|
.hanzo-id-sms-consent { color: var(--muted-foreground); font-size: var(--text-xs); line-height: var(--leading-relaxed); }
|
|
.hanzo-id-sms-consent p { margin: 0 0 6px; }
|
|
.hanzo-id-sms-consent-links { margin: 0; }
|
|
.hanzo-id-sms-consent a { color: var(--text-primary); }
|
|
|
|
/* ── Inline message ────────────────────────────────────────────────── */
|
|
/* Red is one of the two hues @hanzo/design permits, and these are the system's
|
|
own state tokens — the same #fca5a5 pay.hanzo.ai renders errors in. The
|
|
informational variant used to be a blue (#78b8ff) that exists nowhere in the
|
|
system; on a monochrome surface "information" is a neutral card. */
|
|
|
|
.hanzo-id-error,
|
|
.hanzo-id-info {
|
|
padding: 12px;
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-sm);
|
|
margin: 0;
|
|
}
|
|
.hanzo-id-error {
|
|
background: var(--state-error-bg);
|
|
color: var(--state-error-text);
|
|
border: 1px solid var(--state-error);
|
|
}
|
|
.hanzo-id-info {
|
|
background: var(--card);
|
|
color: var(--text-secondary);
|
|
border: 1px solid var(--white-10);
|
|
}
|
|
|
|
/* ── Loading ───────────────────────────────────────────────────────── */
|
|
/* `.hanzo-id-spinner` used to be a class with NO rule behind it: the loading
|
|
state measured 0px tall and was invisible on every portal. */
|
|
|
|
.hanzo-id-spinner {
|
|
width: 28px;
|
|
height: 28px;
|
|
margin: 0 auto;
|
|
border: 2px solid var(--white-15);
|
|
border-top-color: var(--primary);
|
|
border-radius: var(--radius-full);
|
|
animation: hanzo-id-spin 700ms linear infinite;
|
|
}
|
|
@keyframes hanzo-id-spin { to { transform: rotate(360deg); } }
|
|
|
|
/* ── Device-authorization approval ─────────────────────────────────── */
|
|
|
|
.hanzo-id-device main { gap: 18px; }
|
|
.hanzo-id-device-prompt {
|
|
color: var(--muted-foreground);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-relaxed);
|
|
margin: 0;
|
|
}
|
|
.hanzo-id-device-prompt strong { color: var(--text-primary); }
|
|
.hanzo-id-device-code {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xl);
|
|
letter-spacing: var(--tracking-widest);
|
|
text-transform: uppercase;
|
|
}
|
|
.hanzo-id-device-confirm {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
color: var(--muted-foreground);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-relaxed);
|
|
}
|
|
|
|
/* ── Forced TOTP enrollment ────────────────────────────────────────── */
|
|
|
|
.hanzo-id-mfa-enroll { display: flex; flex-direction: column; gap: 16px; }
|
|
.hanzo-id-mfa-enroll h2 { margin: 0; font-size: var(--text-xl); }
|
|
.hanzo-id-mfa-qr {
|
|
align-self: center;
|
|
background: var(--pure-white);
|
|
padding: 12px;
|
|
border-radius: var(--radius-lg);
|
|
width: 220px;
|
|
height: 220px;
|
|
box-sizing: content-box;
|
|
}
|
|
.hanzo-id-mfa-qr svg { width: 100%; height: 100%; display: block; }
|
|
.hanzo-id-mfa-manual { font-size: var(--text-sm); color: var(--muted-foreground); }
|
|
.hanzo-id-mfa-manual summary { cursor: pointer; }
|
|
.hanzo-id-mfa-secret,
|
|
.hanzo-id-mfa-recovery code {
|
|
display: inline-block;
|
|
margin-top: 8px;
|
|
padding: 6px 10px;
|
|
background: var(--white-05);
|
|
border: 1px solid var(--white-10);
|
|
border-radius: var(--radius-sm);
|
|
font-family: var(--font-mono);
|
|
letter-spacing: var(--tracking-widest);
|
|
word-break: break-all;
|
|
}
|
|
.hanzo-id-mfa-recovery { font-size: var(--text-sm); color: var(--muted-foreground); line-height: var(--leading-relaxed); }
|
|
.hanzo-id-mfa-recovery code { letter-spacing: var(--tracking-normal); }
|
|
|
|
/* ── Social / Web3 sign-in ─────────────────────────────────────────── */
|
|
/* These are `.hanzo-id-btn.ghost` — there is no separate social-button surface. */
|
|
|
|
.hanzo-id-social { display: flex; flex-direction: column; gap: 10px; }
|
|
|
|
/* Wallet chain chooser — revealed under the single "Connect Wallet" button when
|
|
the injected chain is ambiguous. Indented so the EVM/Solana options read as
|
|
children of the wallet entry. */
|
|
.hanzo-id-wallet-chains {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-left: 4px;
|
|
padding-left: 12px;
|
|
border-left: 1px solid var(--white-10);
|
|
}
|
|
|
|
/* Labeled divider between social row and email form. */
|
|
.hanzo-id-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
text-align: center;
|
|
color: var(--muted-foreground);
|
|
font-size: var(--text-sm);
|
|
}
|
|
.hanzo-id-divider::before,
|
|
.hanzo-id-divider::after {
|
|
content: '';
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--white-10);
|
|
}
|
|
.hanzo-id-divider span { padding: 0 12px; }
|
|
|
|
/* ── Signed-in portal: the apps launcher ───────────────────────────── */
|
|
/* These rules used to be an inline `style={{…}}` object on Portal.tsx carrying
|
|
rgba(255,255,255,0.14), borderRadius 12, fontSize 13, fontWeight 600 and two
|
|
bare opacities — six invented values for facts the token layer already
|
|
states. They are classes now for the same reason as everything else in this
|
|
file: a surface must not depend on where it is mounted. */
|
|
|
|
.hanzo-id-portal main { width: 100%; max-width: 760px; }
|
|
|
|
.hanzo-id-apps {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
|
|
gap: var(--space-3);
|
|
margin-top: var(--space-6);
|
|
}
|
|
|
|
.hanzo-id-applink {
|
|
display: block;
|
|
padding: var(--space-4);
|
|
/* A tile whose affordance is carried by its label, not its edge — so this is
|
|
the decorative hairline rung, not the 3:1 control boundary. --white-15 is
|
|
the ladder rung the old rgba(255,255,255,0.14) was approximating. */
|
|
border: 1px solid var(--white-15);
|
|
border-radius: var(--radius-lg);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: border-color var(--duration-fast) var(--ease-out);
|
|
}
|
|
.hanzo-id-applink:hover { border-color: var(--border-strong); text-decoration: none; }
|
|
.hanzo-id-applink-name { display: flex; justify-content: space-between; font-weight: var(--weight-semibold); }
|
|
.hanzo-id-applink-name span[aria-hidden] { color: var(--text-tertiary); }
|
|
.hanzo-id-applink-desc { color: var(--muted-foreground); font-size: var(--text-sm); margin-top: var(--space-1); }
|
|
|
|
/* The account control is a control, not a bar: cap it so the trigger (and the
|
|
menu, which matches the trigger's width) reads at the size it does on every
|
|
other Hanzo surface instead of spanning the whole 760px column. */
|
|
.hanzo-id-portal-account { margin-top: var(--space-6); max-width: 260px; }
|
|
|
|
/* ── Onboarding flow ───────────────────────────────────────────────── */
|
|
|
|
.hanzo-id-onboarding { display: flex; flex-direction: column; gap: 24px; }
|
|
.hanzo-id-onboarding-head { display: flex; flex-direction: column; gap: 6px; }
|
|
.hanzo-id-onboarding-head h1,
|
|
.hanzo-id-onboarding-done h1 { margin: 0; font-size: var(--text-2xl); letter-spacing: var(--tracking-tight); }
|
|
.hanzo-id-onboarding-body { display: flex; flex-direction: column; gap: 16px; }
|
|
.hanzo-id-onboarding-done { display: flex; flex-direction: column; gap: 16px; }
|
|
|
|
.hanzo-id-stepdots { display: flex; gap: 8px; }
|
|
.hanzo-id-stepdots span {
|
|
flex: 1;
|
|
height: 4px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--white-10);
|
|
}
|
|
.hanzo-id-stepdots span.on { background: var(--primary); }
|
|
|
|
.hanzo-id-slug-preview {
|
|
color: var(--muted-foreground);
|
|
font-size: var(--text-sm);
|
|
margin: -8px 0 0;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.hanzo-id-onboarding-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
.hanzo-id-onboarding-actions .hanzo-id-btn { flex: 1; min-width: 120px; }
|
|
|
|
.hanzo-id-summary { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; margin: 0; }
|
|
.hanzo-id-summary dt { color: var(--muted-foreground); font-size: var(--text-sm); }
|
|
.hanzo-id-summary dd { margin: 0; font-size: var(--text-sm); font-family: var(--font-mono); }
|
|
|
|
/* Consent step */
|
|
.hanzo-id-consent { display: flex; flex-direction: column; gap: 16px; }
|
|
.hanzo-id-consent p { margin: 0; color: var(--muted-foreground); font-size: var(--text-sm); line-height: var(--leading-normal); }
|
|
.hanzo-id-consent-check {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
border: 1px solid var(--border-control);
|
|
border-radius: var(--radius-lg);
|
|
cursor: pointer;
|
|
font-size: var(--text-sm);
|
|
}
|
|
.hanzo-id-consent-check input { margin-top: 2px; accent-color: var(--primary); }
|
|
|
|
/* Plan step — one card per catalog plan + the pay-as-you-go card */
|
|
.hanzo-id-plans { display: flex; flex-direction: column; gap: 10px; }
|
|
.hanzo-id-plan {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 4px;
|
|
min-height: 44px;
|
|
padding: 14px 16px;
|
|
border: 1px solid var(--border-control);
|
|
border-radius: var(--radius-lg);
|
|
background: var(--white-05);
|
|
color: var(--foreground);
|
|
font: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: background 0.15s ease;
|
|
}
|
|
.hanzo-id-plan:hover { background: var(--white-10); }
|
|
.hanzo-id-plan:disabled { opacity: 0.6; cursor: default; }
|
|
.hanzo-id-plan.popular { border-color: var(--border-selected); }
|
|
.hanzo-id-plan-badge {
|
|
align-self: flex-end;
|
|
margin: -4px 0 -18px;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--primary);
|
|
color: var(--primary-foreground);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
}
|
|
.hanzo-id-plans-empty { margin: 0; color: var(--muted-foreground); font-size: var(--text-sm); }
|
|
.hanzo-id-plan-name { font-weight: 600; }
|
|
.hanzo-id-plan-price { font-size: var(--text-sm); }
|
|
.hanzo-id-plan-price em { font-style: normal; color: var(--muted-foreground); }
|
|
.hanzo-id-plan-desc { color: var(--muted-foreground); font-size: var(--text-sm); }
|