mirror of
https://github.com/hanzoai/design.git
synced 2026-10-30 14:58:37 +00:00
776 lines
25 KiB
CSS
776 lines
25 KiB
CSS
/* @hanzo/design/roles.css — the shared ROLE layer.
|
|
*
|
|
* Hand-authored, and a pure consumer of the token layer: every colour, size,
|
|
* distance and duration below is a var() off tokens/*.css, so the knobs a
|
|
* reader turns through @hanzo/appearance — --type-scale, --type-ratio,
|
|
* --density — reach every role here for free.
|
|
*
|
|
* OPT-IN. It is not part of styles.css and nothing imports it implicitly.
|
|
* A surface takes it deliberately:
|
|
*
|
|
* @layer base, roles;
|
|
* @import '@hanzo/design/roles.css' layer(roles);
|
|
*
|
|
* These are the roles no component owns. Anything with behaviour — a tab, a
|
|
* disclosure, an input, a spinner — is an @hanzo/ui component and is not here;
|
|
* what is here is pure presentation, which is why it can be a class.
|
|
*
|
|
* base.css already grants the focus ring, the transition and the reduced-motion
|
|
* zeroing on :where(a,button,…). Roles restate none of it.
|
|
*
|
|
* TWO KINDS OF LITERAL are allowed, and only two, because neither has a token
|
|
* that could carry it:
|
|
* - an ACCESSIBILITY FLOOR — 44px for a thumb, 24px for a pointer, the 6px
|
|
* mark drawn inside one. A floor is a physical measurement of a hand; it
|
|
* must not move when a reader retunes their type or density.
|
|
* - a STRUCTURAL constant — the 1px hairline, a breakpoint (a media query
|
|
* cannot read a custom property), and the unitless line-height ratios,
|
|
* written as the fractions they are so the relation survives a retune.
|
|
* Every other value is a token.
|
|
*/
|
|
@layer roles {
|
|
/* ── type ──────────────────────────────────────────────────────────────
|
|
Structure is @hanzo/ui's H1/Paragraph; the ROLE — which rung, which ink,
|
|
which tracking — is here, said once instead of at every call site. */
|
|
|
|
/* The page heading. Three rungs of one ramp, so retuning --type-scale moves
|
|
all three together and none can drift out of relation with the others.
|
|
|
|
HIERARCHY BY SCALE, NOT BY WEIGHT. --type-hero is 600/1.05, which at
|
|
display sizes closes the counters and reads as a raised voice. A display
|
|
line already carries at size, so it is set in book with leading that lets
|
|
it breathe; medium stays for UI type small enough to need it.
|
|
|
|
ONE INK. The heading owns its colour; a gradient clip or a dimmed second
|
|
line paints it at half strength. */
|
|
.display {
|
|
font: var(--type-hero);
|
|
font-size: var(--text-4xl);
|
|
font-weight: 497;
|
|
line-height: 1.19;
|
|
letter-spacing: -0.012em;
|
|
color: var(--foreground);
|
|
text-wrap: balance;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.display {
|
|
font-size: var(--text-5xl);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.display {
|
|
font-size: var(--text-6xl);
|
|
}
|
|
}
|
|
|
|
/* The section heading — the rung below the display, in the display's book
|
|
weight, and one size at every width: a section heading is deliberately not
|
|
a page heading and must not grow into one. The leading is stated because
|
|
it is NOT the display's. */
|
|
.title {
|
|
font: var(--type-hero);
|
|
font-size: var(--text-3xl);
|
|
font-weight: 497;
|
|
line-height: 1.22;
|
|
letter-spacing: -0.01em;
|
|
color: var(--foreground);
|
|
text-wrap: balance;
|
|
}
|
|
|
|
/* One rung quieter, for a heading under another section's. Same role, same
|
|
face, same tracking — only where it stands on the ramp moves, so it is an
|
|
attribute rather than a second class to keep in agreement with this one. */
|
|
.title[data-quiet] {
|
|
font-size: var(--text-2xl);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
/* The two lines that travel with a heading: the small caps label above one,
|
|
and the paragraph under it. */
|
|
.eyebrow {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--weight-medium);
|
|
line-height: calc(1.25 / 0.875);
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.lede {
|
|
font-size: var(--text-base);
|
|
line-height: 1.65;
|
|
letter-spacing: 0;
|
|
color: var(--muted-foreground);
|
|
text-wrap: pretty;
|
|
}
|
|
|
|
/* A title and its lede sit one beat apart, wherever they stand. */
|
|
.eyebrow + .title,
|
|
.title + .lede {
|
|
margin-top: calc(var(--space-3) + var(--space-1) / 2);
|
|
}
|
|
|
|
.lede + .lede {
|
|
margin-top: calc(var(--space-2) + var(--space-1) / 2);
|
|
}
|
|
|
|
/* The category line under the promise: the one sentence that names what a
|
|
product IS, so it is read second and set bigger and brighter than a lede,
|
|
stepping up a rung at 640 the way the display above it does. */
|
|
.claim {
|
|
font-size: var(--text-2xl);
|
|
line-height: var(--leading-snug);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.claim {
|
|
font-size: var(--text-3xl);
|
|
}
|
|
}
|
|
|
|
/* A link in quiet text — fine print, the line under a form. The sentence is
|
|
dim, so its links take full ink and an underline at rest: in text that
|
|
quiet, colour alone does not say "link". A button inside such a sentence
|
|
(`.inline`) reads as one of its links. */
|
|
.quiet a,
|
|
.quiet .inline {
|
|
color: var(--foreground);
|
|
text-underline-offset: 3px;
|
|
}
|
|
|
|
.quiet a:not(:is(nav, aside, header, [role=navigation]) a),
|
|
.quiet .inline {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.quiet .inline {
|
|
padding: 0;
|
|
border: 0;
|
|
background: none;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* ── controls ──────────────────────────────────────────────────────────
|
|
Three members of one set: the button, the outline, and the quiet link.
|
|
A caller sets a STATE on the shape rather than choosing between two
|
|
classes that would have to be kept in agreement. */
|
|
|
|
/* The quiet weight is chrome glass; the filled weight drops the border
|
|
outright instead of making it transparent — a transparent border is still a
|
|
border, and it would make the loudest control on the page 2px wider than
|
|
the quiet one beside it. */
|
|
.action {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--space-2);
|
|
min-height: 44px;
|
|
padding-inline: var(--space-6);
|
|
border: 1px solid var(--white-14);
|
|
border-radius: var(--radius-full);
|
|
font-size: var(--text-sm);
|
|
/* A rung carries its own leading. Naming the size alone leaves the line box
|
|
to whatever an ancestor sets, which is how one control ends up a pixel
|
|
and a half taller than the one beside it. */
|
|
line-height: calc(1.25 / 0.875);
|
|
font-weight: var(--weight-medium);
|
|
color: var(--foreground);
|
|
background: color-mix(in srgb, var(--neutral-900) 65%, transparent);
|
|
-webkit-backdrop-filter: var(--chrome-blur);
|
|
backdrop-filter: var(--chrome-blur);
|
|
text-decoration: none;
|
|
box-shadow: inset 0 1px 0 0 var(--white-14), 0 4px 16px -2px color-mix(in srgb, var(--pure-black) 30%, transparent);
|
|
transition: all var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
.action:hover {
|
|
background: color-mix(in srgb, var(--neutral-800) 80%, transparent);
|
|
border-color: var(--white-22);
|
|
color: var(--pure-white);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.action[data-fill] {
|
|
border: 0;
|
|
background-color: var(--pure-white);
|
|
color: var(--pure-black);
|
|
box-shadow: 0 4px 20px -2px var(--white-22), 0 2px 8px 0 color-mix(in srgb, var(--pure-black) 40%, transparent);
|
|
}
|
|
|
|
.action[data-fill]:hover {
|
|
opacity: 0.92;
|
|
background-color: var(--neutral-100);
|
|
color: var(--pure-black);
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* On the light ground the dark glass drew a grey slab and the white fill
|
|
vanished into the page: the quiet weight is white glass with a dark edge,
|
|
the loud one is ink. */
|
|
.light .action {
|
|
border-color: color-mix(in srgb, var(--pure-black) 14%, transparent);
|
|
background: color-mix(in srgb, var(--pure-white) 72%, transparent);
|
|
color: var(--foreground);
|
|
box-shadow: 0 2px 10px -4px color-mix(in srgb, var(--pure-black) 18%, transparent);
|
|
}
|
|
|
|
.light .action:hover {
|
|
border-color: color-mix(in srgb, var(--pure-black) 26%, transparent);
|
|
background: var(--pure-white);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.light .action[data-fill],
|
|
.light .action[data-fill]:hover {
|
|
background-color: var(--foreground);
|
|
color: var(--background);
|
|
box-shadow: 0 4px 16px -6px color-mix(in srgb, var(--pure-black) 40%, transparent);
|
|
}
|
|
|
|
/* A word in an outline. Not an action — it carries no fill and no weight —
|
|
so the padding is the caller's, because a chip and a badge are the same
|
|
outline at different sizes. It brightens on approach only as a link. */
|
|
.pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
border: 1px solid var(--white-08);
|
|
border-radius: var(--radius-full);
|
|
font-size: var(--text-sm);
|
|
line-height: calc(1.25 / 0.875);
|
|
color: var(--neutral-300);
|
|
background: var(--white-03);
|
|
text-decoration: none;
|
|
}
|
|
|
|
a.pill {
|
|
transition: border-color var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
a.pill:hover {
|
|
border-color: var(--white-20);
|
|
color: var(--pure-white);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.light .pill {
|
|
border-color: var(--border);
|
|
background: var(--glass);
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.light a.pill:hover {
|
|
border-color: var(--border-strong);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
/* The quiet link — "View all →", "See full pricing ↗". The third member of
|
|
the set, and the hover is the reason it is a rule rather than props. */
|
|
.more {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-sm);
|
|
line-height: calc(1.25 / 0.875);
|
|
font-weight: var(--weight-medium);
|
|
color: var(--neutral-300);
|
|
text-decoration: none;
|
|
transition: color var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
.more:hover {
|
|
color: var(--pure-white);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.light .more {
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.light .more:hover {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
/* ── surfaces ──────────────────────────────────────────────────────────
|
|
Card has no lift/ground API, so these are roles rather than components. */
|
|
|
|
/* A card. The defaults change nothing: no ground, and a hover ground that is
|
|
whatever the resting one is, so a card naming only --leaf-ground gets a
|
|
ground and no lift. The rise is `translate` rather than `transform` on
|
|
purpose — an animation library owns `transform` on most of these, and the
|
|
two properties compose instead of replacing each other. */
|
|
.leaf {
|
|
--leaf-pad: var(--space-4);
|
|
--leaf-edge: transparent;
|
|
--leaf-edge-hover: var(--white-15);
|
|
--leaf-lift: var(--pure-white);
|
|
--leaf-ground: transparent;
|
|
--leaf-ground-hover: var(--leaf-ground);
|
|
--leaf-rise: 0;
|
|
display: block;
|
|
height: 100%;
|
|
padding: var(--leaf-pad);
|
|
border: 1px solid var(--leaf-edge);
|
|
border-radius: var(--radius-xl);
|
|
background-color: var(--leaf-ground);
|
|
text-decoration: none;
|
|
transition:
|
|
border-color var(--duration-fast) var(--ease-in-out),
|
|
background-color var(--duration-fast) var(--ease-in-out),
|
|
translate var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
.leaf:hover {
|
|
border-color: var(--leaf-edge-hover);
|
|
background-color: var(--leaf-ground-hover);
|
|
translate: 0 var(--leaf-rise);
|
|
}
|
|
|
|
.leaf [data-lift] {
|
|
transition: color var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
.leaf:hover [data-lift] {
|
|
color: var(--leaf-lift);
|
|
}
|
|
|
|
.light .leaf {
|
|
--leaf-lift: var(--foreground);
|
|
}
|
|
|
|
/* A surface whose ground answers approach. IT ADDS NO GEOMETRY — no edge and
|
|
no corner — because it was born as the cell of a ruled grid, which rules
|
|
its own lines with a 1px gap over a neutral ground. Giving it .leaf instead
|
|
would put a corner on every tile and break the flush rule the grid is made
|
|
of. The ink is the same shape as the ground, and [data-lift] is the ink of
|
|
what is INSIDE, because a card whose own ink holds while a name and an
|
|
arrow inside it brighten is most of a page's cards. */
|
|
.cell {
|
|
--cell-lift: var(--pure-white);
|
|
--cell-ground: var(--pure-black);
|
|
--cell-ground-hover: var(--neutral-950);
|
|
--cell-ink: inherit;
|
|
--cell-ink-hover: var(--cell-ink);
|
|
background-color: var(--cell-ground);
|
|
color: var(--cell-ink);
|
|
transition:
|
|
background-color var(--duration-fast) var(--ease-in-out),
|
|
color var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
.cell:hover {
|
|
background-color: var(--cell-ground-hover);
|
|
color: var(--cell-ink-hover);
|
|
}
|
|
|
|
.cell [data-lift] {
|
|
transition: color var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
.cell:hover [data-lift] {
|
|
color: var(--cell-lift);
|
|
}
|
|
|
|
/* Chrome material: the bars the page passes behind, glass over a blur. The
|
|
-webkit- prefix is the one Safari still needs, and a prefix belongs in a
|
|
sheet rather than in every component's inline style. */
|
|
.chrome {
|
|
background: var(--chrome);
|
|
-webkit-backdrop-filter: var(--chrome-blur);
|
|
backdrop-filter: var(--chrome-blur);
|
|
-webkit-tap-highlight-color: transparent;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
/* A ruled backdrop: a decorative grid behind a hero. The two things a call
|
|
site varies are the two knobs. Ruled in --foreground, so it reads on
|
|
either ground. */
|
|
.lattice {
|
|
--lattice-size: 30px;
|
|
--lattice-opacity: 0.05;
|
|
--lattice-ink: color-mix(in srgb, var(--foreground) calc(var(--lattice-opacity) * 100%), transparent);
|
|
background-image:
|
|
linear-gradient(to right, var(--lattice-ink) 1px, transparent 1px),
|
|
linear-gradient(to bottom, var(--lattice-ink) 1px, transparent 1px);
|
|
background-size: var(--lattice-size) var(--lattice-size);
|
|
}
|
|
|
|
/* ── marks on approach ─────────────────────────────────────────────────
|
|
[data-lift] above is one of three. [data-show] is a line only OFFERED on
|
|
approach; [data-step] is a mark that steps toward where it goes.
|
|
|
|
NO ROLE ON THE PARENT, and that is the difference from [data-lift]. Ink is
|
|
the card's decision, so a card names it; presence and position are the
|
|
mark's, and the thing being approached is either the LINK the mark is
|
|
inside or whatever it sits directly in. Two selectors answer both.
|
|
|
|
THE RESTING HALF IS IN THE RULE, and that is the whole reason these exist:
|
|
an inline `opacity: 0` out-ranks the :hover that has to undo it, so a
|
|
reveal written as a style attribute beside a utility never reveals.
|
|
|
|
[data-step] names only the step. The transition rides the call site,
|
|
because these marks change ink AND position and a rule here would REPLACE
|
|
the `.leaf [data-lift]` transition rather than add to it. */
|
|
[data-show] {
|
|
opacity: 0;
|
|
transition: opacity var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
a:hover [data-show],
|
|
:hover > [data-show] {
|
|
opacity: 1;
|
|
}
|
|
|
|
a:hover [data-step],
|
|
:hover > [data-step] {
|
|
translate: 2px 0;
|
|
}
|
|
|
|
/* ── the rest ──────────────────────────────────────────────────────────── */
|
|
|
|
/* A RUN: a light crosses a set of things that are all true at once. Prose
|
|
states invariants one at a time; a run lays the set out and sweeps it, so
|
|
several are lit together and none is ever "next". The container says
|
|
`run`, each DIRECT child sets `--i` to its index. `--run-step` is the delay
|
|
per index (short against the 5.4s cycle; an ORDERED set takes a step near
|
|
cycle/n), `--run-rest` the colour the sweep returns to, because the
|
|
animation owns `color` for the whole cycle. Colour only, so it composes
|
|
with an entry reveal that owns transform and opacity. */
|
|
.run > * {
|
|
animation: run 5.4s var(--ease-in-out) infinite;
|
|
animation-delay: calc(var(--i, 0) * var(--run-step, 0.22s));
|
|
}
|
|
|
|
@keyframes run {
|
|
0%,
|
|
26%,
|
|
100% {
|
|
color: var(--run-rest, var(--neutral-400));
|
|
border-color: var(--neutral-800);
|
|
}
|
|
8% {
|
|
color: var(--foreground);
|
|
border-color: var(--muted-foreground);
|
|
}
|
|
}
|
|
|
|
/* PROSE: elements authored as text — a document, or a chat message rendered
|
|
from markdown. One role, two measures: a document's body by default, and
|
|
`data-compact` for a message, which sits in a bubble sized to its content,
|
|
so its block spacing is set on the children and cleared at the ends. A
|
|
contents list in a <nav> is chrome, and its links do not underline. */
|
|
.prose a {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.prose a:not(:is(nav, aside, header, [role=navigation]) a) {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.prose ul { list-style: disc; }
|
|
.prose ol { list-style: decimal; }
|
|
|
|
.prose code {
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.prose hr {
|
|
border: 0;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* A table is the one thing that can outgrow its column, and nothing wraps
|
|
it, so the scroll goes on the table itself. */
|
|
.prose table {
|
|
display: block;
|
|
overflow-x: auto;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.prose :is(th, td) {
|
|
border: 1px solid var(--border);
|
|
text-align: start;
|
|
}
|
|
|
|
.prose:not([data-compact]) {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-4);
|
|
font-size: var(--text-base);
|
|
line-height: var(--leading-relaxed);
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.prose:not([data-compact]) a { text-underline-offset: 4px; }
|
|
|
|
.prose:not([data-compact]) :is(strong, th) {
|
|
font-weight: var(--weight-medium);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.prose:not([data-compact]) :is(h2, h3) {
|
|
font-weight: var(--weight-medium);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.prose:not([data-compact]) h2 {
|
|
margin-top: var(--space-6);
|
|
font-size: var(--text-xl);
|
|
}
|
|
|
|
.prose:not([data-compact]) h3 {
|
|
margin-top: var(--space-4);
|
|
font-size: var(--text-base);
|
|
}
|
|
|
|
.prose:not([data-compact]) :is(ul, ol) {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
padding-left: var(--space-5);
|
|
}
|
|
|
|
.prose:not([data-compact]) blockquote {
|
|
padding: var(--space-4);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
background-color: var(--secondary);
|
|
}
|
|
|
|
.prose:not([data-compact]) code {
|
|
font-size: var(--text-sm);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.prose:not([data-compact]) table { font-size: var(--text-sm); }
|
|
|
|
.prose:not([data-compact]) :is(th, td) {
|
|
padding: var(--space-2) var(--space-3);
|
|
vertical-align: top;
|
|
}
|
|
|
|
.prose[data-compact] {
|
|
font-size: var(--text-sm);
|
|
line-height: 1.55;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.prose[data-compact] > * { margin-block: 0.6em; }
|
|
.prose[data-compact] > :first-child { margin-block-start: 0; }
|
|
.prose[data-compact] > :last-child { margin-block-end: 0; }
|
|
|
|
.prose[data-compact] :is(h1, h2, h3, h4) {
|
|
font-weight: var(--weight-semibold);
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.prose[data-compact] h1 { font-size: 1.35em; }
|
|
.prose[data-compact] h2 { font-size: 1.2em; }
|
|
.prose[data-compact] h3 { font-size: 1.08em; }
|
|
.prose[data-compact] h4 { font-size: 1em; }
|
|
|
|
.prose[data-compact] :is(ul, ol) { padding-inline-start: 1.4em; }
|
|
.prose[data-compact] li,
|
|
.prose[data-compact] li > :is(ul, ol) { margin-block: 0.25em; }
|
|
|
|
.prose[data-compact] a { text-underline-offset: 2px; }
|
|
.prose[data-compact] :is(strong, th) { font-weight: var(--weight-semibold); }
|
|
.prose[data-compact] em { font-style: italic; }
|
|
.prose[data-compact] del { text-decoration: line-through; opacity: 0.7; }
|
|
|
|
/* Code sits on the ground one step up, so a fence reads as a different KIND
|
|
of text rather than as a box. A fenced block scrolls sideways on its own:
|
|
wrapping code changes what it says. */
|
|
.prose[data-compact] code {
|
|
font-size: 0.92em;
|
|
padding: 0.12em 0.34em;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--secondary);
|
|
}
|
|
|
|
.prose[data-compact] pre {
|
|
overflow-x: auto;
|
|
padding: 0.75rem 0.85rem;
|
|
border-radius: var(--radius-md);
|
|
background: var(--secondary);
|
|
}
|
|
|
|
.prose[data-compact] pre code {
|
|
padding: 0;
|
|
background: none;
|
|
font-size: 0.88em;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.prose[data-compact] blockquote {
|
|
padding-inline-start: 0.9em;
|
|
border-inline-start: 2px solid var(--border);
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.prose[data-compact] :is(th, td) { padding: 0.35em 0.6em; }
|
|
.prose[data-compact] th { background: var(--secondary); }
|
|
|
|
/* Two controls that close a section: stacked on a phone, side by side from
|
|
40rem. GRID, not flex — a row of two is a track list, and
|
|
`grid-auto-flow: column` is the direction said once, while the column case
|
|
needs nothing said at all because a grid is already one. The breakpoint is
|
|
spelt out because a media query cannot read a custom property. */
|
|
.pair {
|
|
display: grid;
|
|
gap: var(--space-4);
|
|
justify-items: center;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
@media (min-width: 40rem) {
|
|
.pair {
|
|
grid-auto-flow: column;
|
|
}
|
|
}
|
|
|
|
/* A run of dots, one of them current. The DOT is drawn inside the target
|
|
rather than being it: a 6px dot sized as the control would obey the
|
|
pointer floor by stretching into a 6x24 bar. Sized as a target with a dot
|
|
in it, both are right, and the WIDTH follows the same pointer question the
|
|
height already asks. */
|
|
.dot {
|
|
--dot-rest: var(--white-22);
|
|
--dot-hover: var(--white-60);
|
|
--dot-on: var(--pure-white);
|
|
display: grid;
|
|
place-items: center;
|
|
height: 24px;
|
|
width: 24px;
|
|
border: 0;
|
|
background: none;
|
|
border-radius: var(--radius-full);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dot > * {
|
|
height: 6px;
|
|
width: 6px;
|
|
border-radius: var(--radius-full);
|
|
background-color: var(--dot-rest);
|
|
transition: background-color var(--duration-fast) var(--ease-in-out);
|
|
}
|
|
|
|
.dot:hover > * {
|
|
background-color: var(--dot-hover);
|
|
}
|
|
|
|
.dot[data-on] > * {
|
|
background-color: var(--dot-on);
|
|
}
|
|
|
|
@media (pointer: coarse) {
|
|
.dot {
|
|
width: 44px;
|
|
}
|
|
}
|
|
|
|
/* A horizontally-scrolling row that does not draw its scrollbar. The rule
|
|
describes what the scroller has on it rather than which part is missing. */
|
|
.bare {
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.bare::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
/* The skip link — out of the page until a keyboard reaches it, then the first
|
|
control on it. It is NOT `display: none` and never can be: a hidden skip
|
|
link is one a keyboard cannot focus, so the whole affordance goes with it.
|
|
A 1px box clipped to nothing is the construction that stays reachable. */
|
|
.skip {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
margin: -1px;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
border-width: 0;
|
|
}
|
|
|
|
.skip:focus {
|
|
width: auto;
|
|
height: auto;
|
|
margin: 0;
|
|
overflow: visible;
|
|
clip-path: none;
|
|
white-space: normal;
|
|
top: var(--space-4);
|
|
left: var(--space-4);
|
|
z-index: var(--z-dropdown);
|
|
padding: var(--space-2) var(--space-4);
|
|
border: 1px solid var(--neutral-700);
|
|
border-radius: var(--radius-full);
|
|
background-color: var(--pure-black);
|
|
font-size: var(--text-sm);
|
|
line-height: calc(1.25 / 0.875);
|
|
color: var(--pure-white);
|
|
}
|
|
|
|
/* `.tap` says "this is a standalone tap target". A link in a paragraph is not
|
|
one — stretching it to 44px would tear the line it sits in — so the floor
|
|
cannot name every a[href], and a link that IS a control has to say so.
|
|
|
|
A TAP TARGET IS A BOX, and the floor applies to the SMALLER SIDE. Height
|
|
alone is half a policy: an icon-only control has no text to widen it, so it
|
|
stays as narrow as its glyph however tall the rule makes it. The box is
|
|
unconditional; only the thumb-sized minimum tracks the pointer.
|
|
|
|
`!important` IS LOAD-BEARING and is not a shortcut. @hanzo/gui writes a
|
|
control's size INLINE, and an inline declaration outranks a stylesheet rule
|
|
on the same property at any specificity — so without it this rule is a
|
|
silent no-op on exactly the controls it exists for. */
|
|
.tap {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
@media (pointer: coarse) {
|
|
.tap {
|
|
min-height: 44px !important;
|
|
min-width: 44px !important;
|
|
}
|
|
}
|
|
|
|
/* Said once here, where the transitions are declared. Every call site used to
|
|
carry its own motion-reduce rule beside every transition — copies of one
|
|
policy, and one of them had been forgotten. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.action,
|
|
.action[data-fill],
|
|
a.pill,
|
|
.more,
|
|
.leaf,
|
|
.leaf [data-lift],
|
|
.cell,
|
|
.cell [data-lift],
|
|
.dot > *,
|
|
[data-show] {
|
|
transition: none;
|
|
}
|
|
|
|
.run > * {
|
|
animation: none;
|
|
}
|
|
|
|
.leaf:hover,
|
|
a:hover [data-step],
|
|
:hover > [data-step] {
|
|
translate: none;
|
|
}
|
|
}
|
|
}
|