Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/docs-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ jobs:
with:
fetch-depth: 0 # the git-revision-date plugin reads each page's history

# The design system in bindings/python/docs/brand is vendored, never edited here: it must still match the hash in its VERSION.md.
- name: Check the vendored design system copy
run: bash bindings/python/docs/brand/verify.sh

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down
6 changes: 3 additions & 3 deletions bindings/python/docs/brand/VERSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
Do not edit these files here. Change them in github.com/humemai/design-system,
then vendor again. `./verify.sh` checks this copy against the hash below.

Commit: 79b1a8b2318ada932dcdebdf57efb414d35b3119
Date: 2026-11-06
Hash: 0071e50a6c4a2714d4b8cf3b3edb75211214b2781bf17468712f4005b9acdf65
Commit: 19edee040a025ba0551bd48dcb30d8b35046afa1
Date: 2026-11-11
Hash: 9d9b6f2c78a75ec59c0cfafc6b7984c258804c97cc8d2c66af98e23b6528af41
71 changes: 71 additions & 0 deletions bindings/python/docs/brand/css/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* HumemAI base rules every page wants and none should write twice. Opt-in:
load it after tokens.css; nothing here sets a colour, a font or a margin on
an element of its own. The focus ring was written twice (the Humem Tower's
stylesheet and the website's globals.css), and the Tower's skip link and its
motion rule had no counterpart on the site (humemai/design-system#14). The
structure follows Stateloop's base and motion files; the skip link is a
HumemAI original, since Stateloop's design system has none. */

/* The focus ring: 2px in --hm-focus, which is 3:1 on every page surface
(scripts/check.py), pulled 2px off the edge so it never covers a border. */
:focus-visible {
outline: 2px solid var(--hm-focus);
outline-offset: 2px;
}

/* A page with a sticky header sets --hm-sticky-offset on :root to the header's
height, so a jump to #main or any anchor lands below the header and a focused
control is not scrolled under it (WCAG 2.4.11). */
html {
scroll-padding-block-start: var(--hm-sticky-offset, 0px);
}

/* Skip link: the first focusable thing on the page, <a class="hm-skip"
href="#main">Skip to content</a>, and the target needs id="main". Hidden
until a keyboard focuses it, so it never shows to a mouse. Focused, it is
fixed to the top left corner with a high z-index, which puts it above a sticky
header (the website's is 40) instead of beneath it, in link ink on the
surface with a focus-colour border. */
.hm-skip:not(:focus) {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
border: 0;
overflow: hidden;
white-space: nowrap;
clip-path: inset(50%);
}

.hm-skip:focus {
position: fixed;
inset-block-start: var(--hm-space-2);
inset-inline-start: var(--hm-space-2);
z-index: 100;
padding: var(--hm-space-2) var(--hm-space-4);
background: var(--hm-surface);
border: 2px solid var(--hm-focus);
border-radius: var(--hm-radius-sm);
color: var(--hm-link);
font-weight: var(--hm-weight-semibold);
text-decoration: underline;
text-underline-offset: 0.2em;
}

/* Reduced motion: a reader who asked for less gets no transition and no
animation, whoever wrote them. A duration of 0.01ms and not 0s, so an
animation still ends in its last frame and its animationend still fires.
Hover lifts that a component writes as a transform are zeroed beside the
component (.hm-card, in components.css). */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-delay: 0s !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
transition-delay: 0s !important;
scroll-behavior: auto !important;
}
}
226 changes: 226 additions & 0 deletions bindings/python/docs/brand/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,230 @@
.hm-card {
transition: none;
}

.hm-card:hover,
.hm-card:focus-visible {
transform: none;
}
}

/* Hint: a term's short explanation. A small ? beside the word, opened by hover
with a mouse and by click, tap or Enter everywhere (humemai/design-system #12,
mirrored from Stateloop's .stl-hint of 2026-11-06). A native title="" tooltip
never shows on a touch screen and a keyboard cannot reach it. The button is a
circle, the one round shape here: a help mark is an icon people recognise by
that shape, not a container. It is --hm-tap-min (24px, the WCAG 2.5.8 floor)
on both sides, and its minimum sizes are set too, because an app's own
button { min-height } would otherwise stretch the circle into an oval. At
999px and narrower it keeps its 24px mark and grows a 44px hit area (the
::after), not its layout. The border is --hm-border-control, 3:1 on every page
surface; hover, focus and the open state turn the border and the mark to the
brand colour, and a keyboard gets the focus ring. The bubble is a popover in
the top layer, so a scrolling table or a sheet cannot clip it; js/hint.js
places it under its button (above it near the bottom) and keeps it on screen,
and it stays hidden until placed so it never flashes in a corner. The hint
sits beside its term, often inside an all-caps, letter-spaced column head, so
the button and the bubble reset every inherited text style: the definition
must read as body text. Only for a word a person cannot act without knowing;
anything needed to act stays visible. */
.hm-hint {
box-sizing: border-box;
display: inline-grid;
place-items: center;
inline-size: var(--hm-tap-min);
block-size: var(--hm-tap-min);
min-inline-size: var(--hm-tap-min);
min-block-size: var(--hm-tap-min);
margin-inline-start: var(--hm-space-1);
padding: 0;
border: 1px solid var(--hm-border-control);
border-radius: var(--hm-radius-pill);
background: transparent;
color: var(--hm-text-muted);
font-family: var(--hm-font-text);
font-size: var(--hm-text-xs);
font-style: normal;
font-weight: var(--hm-weight-semibold);
line-height: 1;
letter-spacing: normal;
text-transform: none;
vertical-align: middle;
cursor: help;
}

.hm-hint:hover,
.hm-hint:focus-visible,
.hm-hint[aria-expanded="true"] {
border-color: var(--hm-brand);
color: var(--hm-brand);
}

.hm-hint:focus-visible {
outline: 2px solid var(--hm-focus);
outline-offset: 2px;
}

.hm-hint__bubble {
box-sizing: border-box;
inset: auto;
margin: 0;
max-inline-size: min(calc(var(--hm-measure) / 2), calc(100vw - 2 * var(--hm-space-4)));
padding: var(--hm-space-2) var(--hm-space-3);
border: 1px solid var(--hm-border-strong);
border-radius: var(--hm-radius-md);
background: var(--hm-surface-raised);
color: var(--hm-text);
font-family: var(--hm-font-text);
font-size: var(--hm-text-sm);
font-style: normal;
font-weight: var(--hm-weight-regular);
line-height: 1.45;
text-align: start;
text-indent: 0;
text-transform: none;
letter-spacing: normal;
word-spacing: normal;
white-space: normal;
box-shadow: var(--hm-shadow-md);
}

.hm-hint__bubble:not([data-placed]) {
visibility: hidden;
}

.hm-hint__bubble a {
color: var(--hm-link);
text-decoration: underline;
text-underline-offset: 0.15em;
}

.hm-hint__bubble a:hover {
color: var(--hm-link-hover);
}

@media (max-width: 999px) {
.hm-hint {
position: relative;
}

.hm-hint::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: var(--hm-tap-comfort);
height: var(--hm-tap-comfort);
transform: translate(-50%, -50%);
}
}

/* Table: a data table inside a scroller that a keyboard can reach. Mirrored
from the Humem Tower's tables (humemai/design-system#14); Stateloop's .stl-table
has no scroller, but its rule holds here too: a scroller has a name and a way
in from the keyboard. Wrap the table in .hm-table-scroll and give the wrapper
role="region", tabindex="0" and an aria-label naming the table; without them
a keyboard cannot scroll a wide table (the benchmark scroller on humem.ai
lacked all three). Only the wrapper scrolls, so the page never does. The
table is border-collapse: separate with no spacing, not collapse, because a
collapsed table drops the borders of a sticky header cell when it scrolls.
Text is --hm-text-sm (14px), the system's table size. Column widths are the
page's own: the Tower sets them by column role, the website by layout, and
neither belongs here. */
.hm-table-scroll {
margin: 0 0 var(--hm-space-4);
overflow-x: auto;
background: var(--hm-surface);
border: 1px solid var(--hm-border);
border-radius: var(--hm-radius-md);
}

.hm-table {
inline-size: 100%;
margin: 0;
border-collapse: separate;
border-spacing: 0;
font-size: var(--hm-text-sm);
line-height: 1.4;
}

.hm-table th,
.hm-table td {
padding: var(--hm-space-1) var(--hm-space-2);
border-bottom: 1px solid var(--hm-border);
text-align: left;
vertical-align: top;
overflow-wrap: break-word;
}

.hm-table th {
background: var(--hm-surface-sunken);
border-bottom: 2px solid var(--hm-border-strong);
font-weight: var(--hm-weight-semibold);
}

.hm-table tbody tr:last-child td {
border-bottom: 0;
}

/* Optional: the header row stays in view while the table scrolls down. It
sticks to the wrapper, so the wrapper needs a height of its own, for example
max-block-size: 70vh; a wrapper as tall as its table has nothing to scroll. */
.hm-table--sticky-head thead th {
position: sticky;
inset-block-start: 0;
z-index: 1;
}

/* A numeric column: against the right edge, so units and decimal points line
up, and in DM Mono. Schibsted Grotesk's default figures are proportional (the
Tower measured "1" at 5.8px and "0" at 10.4px at 17px), and its tabular
figures also widen the punctuation ("1 . 6 h"), so no tabular-nums here: the
cell takes the mono face. The header cell aligns right and keeps its own face. */
.hm-table .hm-num {
text-align: right;
white-space: nowrap;
}

.hm-table td.hm-num {
font-family: var(--hm-font-mono);
}

/* Badge: a state or a label as a pill. The word is always there; the fill and a
symbol are a second cue, never colour alone. Three variants, because the brand
has one voice and one warning: brand (filled, the active or running thing),
danger (the failed or overdue thing), tint (the held, paused or stale thing).
With no variant it is a quiet outline. The pairings (text on its own fill,
border on the page) are in scripts/check.py. A symbol is the consumer's: put it
in a ::before with an empty alt, content: "\25B6\FE0E" / "", so a screen reader
reads the word, not the shape (the Tower maps its nine states this way). */
.hm-badge {
display: inline-flex;
align-items: center;
gap: var(--hm-space-1);
padding: 0 var(--hm-space-2);
background: var(--hm-surface);
border: 1px solid var(--hm-border-control);
border-radius: var(--hm-radius-pill);
color: var(--hm-text-muted);
font-size: var(--hm-text-sm);
font-weight: var(--hm-weight-semibold);
line-height: 1.5;
white-space: nowrap;
}

.hm-badge--brand {
background: var(--hm-brand);
border-color: var(--hm-brand);
color: var(--hm-on-brand);
}

.hm-badge--danger {
border-color: var(--hm-danger);
color: var(--hm-danger);
}

.hm-badge--tint {
background: var(--hm-tint);
border-color: var(--hm-tint-text);
color: var(--hm-tint-text);
}
16 changes: 16 additions & 0 deletions bindings/python/docs/brand/css/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@
--hm-space-20: 5rem;
--hm-space-24: 6rem;

/* Tap targets. 24px is the WCAG 2.5.8 floor, for a mouse; at 999px and
narrower a control is 44px (the Behaviour section of the README). */
--hm-tap-min: 1.5rem; /* 24 */
--hm-tap-comfort: 2.75rem; /* 44 */
/* The height of a button, input or select: --hm-tap-comfort (44) at 999px and
narrower, 36 from 1000px (the media block below), where a pointer is enough.
A control sets min-height: var(--hm-control-h) and pads the rest. */
--hm-control-h: var(--hm-tap-comfort);

--hm-radius-sm: 0.375rem;
--hm-radius-md: 0.625rem;
--hm-radius-lg: 1rem;
Expand All @@ -104,6 +113,7 @@
--hm-surface: var(--hm-neutral-0);
--hm-surface-muted: var(--hm-neutral-50);
--hm-surface-sunken: var(--hm-neutral-100);
--hm-surface-raised: var(--hm-neutral-0); /* what floats above the page: a hint bubble; the border and shadow lift it in light */
--hm-text: var(--hm-neutral-900);
--hm-text-muted: var(--hm-neutral-600);
--hm-text-subtle: var(--hm-neutral-500); /* on --hm-bg and --hm-surface only */
Expand All @@ -119,11 +129,13 @@
--hm-tint-text: var(--hm-oxblood-700);
--hm-focus: var(--hm-oxblood-600);
--hm-danger: var(--hm-danger-light);
--hm-shadow-md: 0 3px 10px color-mix(in srgb, var(--hm-neutral-900) 16%, transparent);
}

@media (min-width: 1000px) {
:root {
--hm-text-base: 1.0625rem; /* 17 */
--hm-control-h: 2.25rem; /* 36 */
}
}

Expand All @@ -136,6 +148,7 @@
--hm-surface: var(--hm-neutral-900);
--hm-surface-muted: var(--hm-neutral-900);
--hm-surface-sunken: var(--hm-neutral-950);
--hm-surface-raised: var(--hm-neutral-800);
--hm-text: var(--hm-neutral-50);
--hm-text-muted: var(--hm-neutral-400);
--hm-text-subtle: var(--hm-neutral-450);
Expand All @@ -151,6 +164,7 @@
--hm-tint-text: var(--hm-oxblood-300);
--hm-focus: var(--hm-oxblood-400);
--hm-danger: var(--hm-danger-dark);
--hm-shadow-md: 0 3px 10px color-mix(in srgb, var(--hm-neutral-950) 70%, transparent);
}
}

Expand All @@ -160,6 +174,7 @@
--hm-surface: var(--hm-neutral-900);
--hm-surface-muted: var(--hm-neutral-900);
--hm-surface-sunken: var(--hm-neutral-950);
--hm-surface-raised: var(--hm-neutral-800);
--hm-text: var(--hm-neutral-50);
--hm-text-muted: var(--hm-neutral-400);
--hm-text-subtle: var(--hm-neutral-450);
Expand All @@ -175,4 +190,5 @@
--hm-tint-text: var(--hm-oxblood-300);
--hm-focus: var(--hm-oxblood-400);
--hm-danger: var(--hm-danger-dark);
--hm-shadow-md: 0 3px 10px color-mix(in srgb, var(--hm-neutral-950) 70%, transparent);
}
Loading
Loading