Skip to content

design(site): publish the mark vocabulary, fix the contrast gate, put every button on one system, and gate all three in CI - #27

Merged
bkearns merged 7 commits into
mainfrom
copy/memory-positioning
Aug 23, 2026
Merged

design(site): publish the mark vocabulary, fix the contrast gate, put every button on one system, and gate all three in CI#27
bkearns merged 7 commits into
mainfrom
copy/memory-positioning

Conversation

@bkearns

@bkearns bkearns commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The headline

Memory servers should be inspectable systems named a property rather than a reason to exist. Worse, "inspectable" is the exact word a sceptic reaches for when arguing that what you have is a review workflow rather than memory — it concedes the ground before the argument starts.

New: Knowledge that survives the session, and the model.

Durability and portability are unambiguously memory, so the category is never in question. It also ladders into the company page — "agent data has gravity" is the same thesis one level up.

The specimen carries the argument the headline avoids

Swapped from a version-sync record to a deferred decision, still open eleven weeks and four sessions later, attached to the entity it belongs to:

fact_text     "deferring the retry backoff to a later pass"
entity        ferrosa-dbaas / SnapshotScheduler
event_time    2026-06-02T14:09Z
valid_until   —   STILL OPEN

A decision an agent made about your project is a fact about your project, and it is precisely the kind that scrolls out of a chat log. Showing one row settles that better than arguing the category would.

The retrieval claim was wrong — and the true number is stronger

The comparison table claimed "RRF over ~11 signals". The fusion config in ferrosa-memory-core/src/hybrid_search.rs actually carries 17 weighted channels across six families:

family channels
vector / ANN 3 — ann, context_ann, document_ann
lexical BM25 / FTS 3 — entity_content_fts, context_bm25, document_bm25
phonetic 2 — phonetic, document_phonetic
graph 3 — pagerank, datalog_frontier, reputation
workspace / recency 2 — workspace, warmth
memory-type structural 4 — scene, profile, foresight, fold

So the table now says 17 weighted channels, and the retrieval section states where similarity actually sits: three of seventeen, with lexical BM25 and full-text carrying exactly as much weight.

That is the honest answer to the "embeddings are hype, full text plus metadata wins" objection — the architecture already agrees with it, the page just never said so. It is also checkable, which a positioning claim is not.

Found, not fixed

hybrid_search.rs opens with /// Configuration for 6-signal RRF fusion weights. That comment counts families and predates eleven of the channels. It is code rather than copy, so it is untouched here — worth a one-line fix in ferrosa-memory.

Verification

  • Every claim checked against ferrosa-memory-core before writing: provenance/derivation in the Datalog layer, propose/preview/confirm/dry_run in the forget path, and the 17 weights above.
  • check-site passes; contrast 127/127 AA.
  • Reviewed rendered at 1440×1000.

One file changed. No structural or CSS work — the sections below the hero are untouched.


Added: drafting-mark icons (scope grew past "memory copy")

Ben asked for the emoji icons to be replaced site-wide and folded into this PR, so it is no longer only a copy change. Retitled accordingly.

51 emoji, not the 38 I first counted

My initial inventory was wrong. The regex required class="feature-icon…" to be followed immediately by >, so it silently skipped 13 wrappers in database/index.html carrying an inline style attribute. A parse-based scan finds 51 icon wrappers. All 51 are now inline SVG; zero emoji remain.

The marks

26 stroke-only marks reused across the 51 slots — 24px grid, fill="none", stroke="currentColor", stroke-width="1.5", round caps, aria-hidden. currentColor is load-bearing: the site ships both polarities and a hex literal would break one.

The hand-drawn quality is geometry, not filters — arcs stop short of closing, rectangles overshoot their corner by half a unit, paired rules drift slightly off parallel. Enough to read as a draftsman's annotation, not enough to become a doodle.

The bigger problem was underneath: 21 different chips

Icon backgrounds came from two places — nine modifier classes (.steel .cyan .cql .graph .lock .index .observe .rust .s3) and twelve one-off inline styles. Neighbouring cards therefore had visibly different chips, which is what Ben spotted on The 10k Result Cap Is Gone and Steadier Under Failure.

The hue carried no meaning, and a decorative rainbow is precisely what "accent is rationed" rules out. One chip now: --surface-2 with a hairline border. The modifier classes stay in the markup but no longer paint.

.pain-icon also never had a box at all — a bare text node sized by font-size — so giving it a background stretched it into a full-card bar. It now shares the same 44px geometry.

Measured from the DOM on all five pages: 51 chips, distinctStyle=1, all 44x44, all containing an svg.

Colour choice

Icons use --muted, not --steel. Steel is the documented line colour, but at 3.79:1 on --bg and drawn over a tinted chip it does not hold the 3:1 non-text floor; --muted does, in both polarities. No icon uses the rationed accent.

Left alone

The ~2,200 box-drawing, arrow and check glyphs (─ ═ → ✓ ✗ ▼) doing real typographic work inside tables and code blocks. Only the 51 in icon wrappers were in scope.

Known weaknesses

  • 26 marks rather than the 12–18 targeted. Eight are single-use. Consolidating needs a design call, so it is captured (t_3d9df756) rather than guessed at.
  • graph standing in for Production Cluster Mode is the weakest fit and the first I would change.
  • bolt appears three times on one page for three different claims — inherited from the source, which used ⚡ for all three.
  • The set reads as a clean icon system rather than visibly sketchy. At 20px a 1-unit irregularity is 0.83px. Pushing further crosses into doodle; the lever exists if you want more.

Also captured: t_956142d1 — document the vocabulary in design-system.html, decide sprite vs. repeated inline, and reconcile with brand.html's separate icon system.


Forge hero size

The Forge tagline rendered larger than every other product landing.

Forge is one of two pages whose <body> carries legacy-doc, the scope added when the last inline page sheets were consolidated. That sheet styles bare h1 for document body copy. Scoping it to .legacy-doc left it at (0,1,1) — an exact tie with .hero-copy h1 — and it lands ~930 lines later, so source order handed the ported rule the win over the shared hero anatomy. It carried three overrides, not one: size, tracking, and a 2rem top margin.

Fixed by excluding the hero from the ported rule (:not(.hero-copy h1), both the base and mobile blocks) rather than restating three values under a heavier selector, which would leave them to drift.

Measured from the DOM:

landings @1440 @390 hero-compact sub-pages @1440
before Forge 64px, others 48px 40px
after 48px on all four 33.6px on all four 40px on all four

Tracking and line-height now match too (-1.68px / 50.4px across all four).


Publishing the mark vocabulary

The 26 marks shipped as inline SVG in five product pages and were documented nowhere, so there was no single place to take one from — the next page needing an icon would copy one out of a comparison table or draw a twenty-seventh.

design-system.html now carries all 26 with the name, the sense each carries, the chip they sit in, and the rules for drawing a new one. That page is the source of truth; adding a mark means a PR here, not a one-off in a page. Closes the deferred t_956142d1.

The contrast gate was checking less than it reported

check-brand-contrast.mjs located each theme block with indexOf on its selector, against the raw file. site.css documents its three theme selectors in its header comment, so those mentions were found first:

":root {"                       offset 2577   line 45  (the real block)
":root[data-theme=\"light\"]"     offset 2149   line 38  (the comment)
"@media (prefers-color-scheme"   offset 2212   line 39  (the comment)

The dark block resolved to slice(2577, 2149) — start after end, an empty string, zero tokens — so every dark pair hit the "token not present, continue" branch and was silently skipped. The third block ran from the comment to EOF, so each token took its last value anywhere in the file rather than its value in that theme.

It reported "127 combinations checked, 0 below AA" while checking none of site.css's dark palette, which is the site's default. Blocks are now located in a comment-stripped copy (comments blanked, not deleted, so offsets still line up).

127 → 183 combinations, still 0 below AA. Nothing was hiding — the closest is --border on --surface at 3.03:1 against a 3.0 floor.

Fifteen buttons were on the wrong system

btn-primary/btn-secondary without the base btn class across six pages. The base class carries the geometry (inline-flex, 13px/600, 6px 12px padding, small radius); the modifier only sets colour. Without it they fell through to ported page styling at 16px, so a closing CTA looked like a different component from the hero button above it.

Found by measurement, not review — the markup looks correct. Nine pages now report one primary and one secondary treatment. No hero was affected; all fifteen sit in closing CTAs, so this is a size fix, not a change to which button is primary.


The gates now run on every PR

All three design gates are wired into Docs CI and verified green in a real run, not only locally.

Check design system contrast     183 combinations checked, 0 below AA
Serve the site                   asserts Chrome, polls until the server answers
Check page anatomy               6 rules x 2 viewports, all 1 distinct style
Check for dead vertical space    9 pages, no band over 200px

check-brand-contrast.mjs is deleted, replaced by the generalized check-contrast.mjs plus a committed design.config.json carrying the same three token sources and the same 29 checks — 183 combinations before and after, so no coverage was lost. The three scripts are vendored from the /ferrosa-design skill rather than fetched, because this repo's runner cannot check out the private repo the skill lives in; design.config.json's header records that, and keeping the two in step is tracked.

Two runner details worth keeping: the workflow asserts Chrome exists up front, so its absence fails with a clear message instead of surfacing three steps later as "probe did not report"; and it polls for the static server rather than sleeping a fixed interval.

Each gate was negative-tested before landing

A gate that cannot fail is decoration.

Injected fault Result
Hero forced to 61px on one page FAIL hero h1 (landings) — 2 distinct computed style(s), exit 1
--muted walked to #55606c FAIL --text-muted on --surface 2.80:1, exit 1
A 420px empty div FAIL 420px empty at y=60–480, exit 1

Adds roughly 50 seconds to the job.

"Memory servers should be inspectable systems" named a property rather than a
reason to exist, and "inspectable" is the word a sceptic reaches for when
arguing that what you have is a review workflow rather than memory.

The page now leads with durability and portability, which nobody disputes is
memory: "Knowledge that survives the session, and the model." That also ladders
into the company page's argument that agent data has gravity — same thesis, one
level down.

## The specimen carries the argument the headline avoids

Swapped to a deferred decision that is still open eleven weeks and four sessions
later, attached to the entity it belongs to. A decision an agent made about your
project is a fact about your project, and it is exactly the kind that scrolls out
of a chat log. Showing one row does more than arguing the category would.

## The retrieval claim was wrong, and the correct number is better

The comparison table said "RRF over ~11 signals". The fusion config in
ferrosa-memory-core/src/hybrid_search.rs has 17 weighted channels in six
families:

    vector / ANN             3    ann, context_ann, document_ann
    lexical BM25 / FTS       3    entity_content_fts, context_bm25, document_bm25
    phonetic                 2    phonetic, document_phonetic
    graph                    3    pagerank, datalog_frontier, reputation
    workspace / recency      2    workspace, warmth
    memory-type structural   4    scene, profile, foresight, fold

So the page now says 17 weighted channels, and the retrieval section states
where similarity actually sits: three of seventeen, with lexical BM25 and
full-text carrying exactly as much weight. That is the honest answer to the
"embeddings are hype, full text plus metadata wins" objection — the architecture
already agrees.

Note for a separate fix: that file's own doc comment reads "Configuration for
6-signal RRF fusion weights", which counts families and predates eleven of the
channels. Code, not copy, so not touched here.
… chip

Emoji render as full-colour, near-photographic glyphs. Against a blueprint
design language that is the most jarring thing on the page, and it is the one
element that looks like it came from somewhere else.

## 51 emoji, not 38

My first inventory said 38. It was wrong: the regex required class="feature-icon…"
to be followed immediately by '>', so it silently skipped 13 wrappers in
database/index.html that carry an inline style attribute. A parse-based scan
finds 51 icon wrappers; all 51 are now inline SVG and none is emoji.

## The marks

26 stroke-only marks reused across the 51 slots, all on a 24px grid with
fill="none", stroke="currentColor", stroke-width 1.5, round caps, and
aria-hidden. currentColor is load-bearing: the site ships both polarities and a
hex literal would break one of them.

The hand-drawn quality is geometry, not filters or a sketchy font — arcs stop
short of closing, rectangles overshoot their corner by half a unit, paired rules
drift slightly off parallel. Disciplined enough to stay a draftsman's
annotation rather than a doodle.

## And one chip, not twenty-one

The bigger inconsistency was underneath. Icon backgrounds came from two places:
nine modifier classes (.steel .cyan .cql .graph .lock .index .observe .rust .s3)
and twelve one-off inline styles — 21 tints in total, so neighbouring cards had
visibly different chips. The hue carried no meaning, and a decorative rainbow is
exactly what "accent is rationed" rules out. One chip now: --surface-2 with a
hairline border. The modifier classes stay in the markup but no longer paint.

.pain-icon also never had a box at all — it was a bare text node sized by
font-size — so giving it a background stretched it into a full-card bar. It now
shares the same 44px geometry.

Measured from the DOM across all five pages: 51 chips, one distinct computed
style, all 44x44, all containing an svg.

Icons use --muted rather than --steel. Steel is the documented line colour, but
at 3.79:1 on --bg and drawn over a tinted chip it does not hold the 3:1 non-text
floor; --muted does, in both polarities. No icon uses the rationed accent.

Left alone: the ~2,200 box-drawing, arrow and check glyphs doing real
typographic work inside tables and code blocks.

Checks: check-site passes, contrast 127/127 AA, nav and examples in sync,
whitespace clean. Rendered in both polarities.
@bkearns bkearns changed the title copy(memory): lead with durability, and correct the retrieval claim copy(memory) + design(site): durability lead, corrected retrieval claim, and drafting-mark icons Aug 19, 2026
The Forge hero rendered at 64px while Database, Memory and the company page
rendered at 48px, with tighter tracking and a 2rem top margin they do not
carry.

Forge is one of two pages whose body carries .legacy-doc, the scope added when
the last inline page sheets were consolidated. That sheet styles bare h1 for
document body copy. Scoping it to .legacy-doc left it at (0,1,1) — an exact tie
with .hero-copy h1 — and it sits ~930 lines later, so source order handed the
ported rule the win over the shared hero anatomy.

Exclude the hero from the ported rule instead of restating font-size,
letter-spacing and margin under a heavier selector, which would leave three
values to keep in sync. Same guard in the mobile block, which had the same tie.

Measured from the DOM: all four landings now 48px / -1.68px / 50.4px at 1440
and 33.6px at 390; the four hero-compact sub-pages stay one step down at 40px.
The marks shipped as inline SVG in five product pages and were documented
nowhere, so there was no single place to take one from — the next page that
needs an icon has to copy one out of a competitor comparison table or draw a
twenty-seventh.

Publish all 26 on design-system.html with the name, the sense each one carries,
the chip they sit in, and the rules for drawing a new one: arcs that stop short
of closing, corners that overshoot, paired rules off parallel. The hand-drawn
quality is geometry, not a filter or a font, which is why it survives both
themes and any scale.

The page is now the source of truth. Adding a mark means a PR here, not a
one-off in a page.

26 marks, one chip treatment, 44x44, all stroke-only on currentColor.
The gate located each theme block with indexOf on its selector, against the raw
file. site.css documents its three theme selectors in its header comment, so
those mentions were found first:

  ":root {"                      offset 2577  (line 45, the real block)
  ":root[data-theme=\"light\"]"    offset 2149  (line 38, the comment)
  "@media (prefers-color-scheme"  offset 2212  (line 39, the comment)

So the dark block resolved to slice(2577, 2149) — start after end, an empty
string, zero tokens, every dark pair silently skipped — and the
prefers-color-scheme block ran from the comment to EOF, swallowing every later
declaration so each token resolved to its LAST value anywhere in the file
rather than its value in that theme.

The gate reported "127 combinations checked, 0 below AA" while checking none of
site.css's dark palette, which is the site's default, and reading both light
palettes off contaminated blocks. That is the failure the file's own header
warns about: a source whose theme block nothing checks is a source that drifts.

Locate blocks in a comment-stripped copy, with comments blanked rather than
deleted so the offsets still line up.

127 -> 183 combinations. Still 0 below AA, so nothing was hiding — the closest
is --border on --surface at 3.03:1 against a 3.0 floor.
Fifteen buttons across six pages carried `btn-primary` or `btn-secondary`
without the base `btn` class. The base class is where the shared button lives —
inline-flex, 13px/600, 6px 12px padding, the small radius; the modifier only
sets colour. Without it those buttons fell through to the ported page styling
and rendered at 16px with different padding, so a closing CTA looked like a
different component from the hero button directly above it.

Found by the anatomy gate, not by looking: `.btn-primary` reported two distinct
computed styles across the site, 16px only on the pages carrying a bare
modifier. Nine pages now report one primary and one secondary treatment:

  primary    13px  radius 8px  padding 6px/12px   9 pages, 1 distinct style
  secondary  13px  radius 8px  padding 6px/12px   9 pages, 1 distinct style

No hero was affected — every one of the fifteen sits in a closing CTA — so this
is a size inconsistency, not a change to which button is primary.
@bkearns bkearns changed the title copy(memory) + design(site): durability lead, corrected retrieval claim, and drafting-mark icons design(site): publish the mark vocabulary, fix the contrast gate, and put every button on one system Aug 19, 2026
The three design gates found real defects last week — a hero rendering 33%
larger than its peers, fifteen buttons falling through to a ported stylesheet,
and a contrast checker reporting 127 clean comparisons while checking none of
the site's default palette. None of that was gated, so all of it could come
back silently. Now it cannot.

  check-contrast.mjs     WCAG AA across every source that declares tokens and
                         every theme block each one ships
  measure-anatomy.mjs    one component, one computed style, across every page
                         that shares it, at 1440 and 390
  find-empty-bands.mjs   no dead vertical band over 200px

All three read design.config.json. The two rendered-page gates need the site
served and Chrome; the workflow asserts the browser is present up front rather
than letting its absence surface as a confusing probe failure three steps later,
and waits for the server instead of sleeping a fixed interval.

check-brand-contrast.mjs is deleted. Its replacement is the generalized,
config-driven script from the /ferrosa-design skill, which covers the same three
token sources and the same 29 pairs: 183 combinations before and after, so this
loses no coverage. The skill lives in a repo this runner cannot check out, so
the scripts are vendored rather than fetched; design.config.json's header says
so, and keeping the two in step is tracked.

Each gate was negative-tested before landing, because a gate that cannot fail is
decoration:

  hero forced to 61px on one page  -> FAIL, 2 distinct styles, exit 1
  --muted walked to #55606c        -> FAIL, 2.80:1 on --surface, exit 1
  a 420px empty div               -> FAIL, "420px empty at y=60-480", exit 1
@bkearns bkearns changed the title design(site): publish the mark vocabulary, fix the contrast gate, and put every button on one system design(site): publish the mark vocabulary, fix the contrast gate, put every button on one system, and gate all three in CI Aug 19, 2026
@bkearns
bkearns merged commit eebea52 into main Aug 23, 2026
1 check passed
jeanluis-ferrosa added a commit that referenced this pull request Aug 23, 2026
The signup button carried padding: 9px 16px, which made it a second
distinct computed style for .btn-secondary and failed the page-anatomy
gate added in #27.

Canonical padding is 6px 12px. The field height is matched by flex
stretch rather than by tuning the button, so both still render at 37px
and the button is now byte-identical to every other secondary button on
the site.

The .signup .btn-secondary rule itself is still needed: #27 did not
remove the ported .btn-secondary duplicate further down site.css, so a
<button> still falls through to it and picks up a var(--divider) border
at 1.23:1 in light.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant