Skip to content

Make the site consistent with itself: one header rhythm, one copy of each component, one value per role - #7

Merged
datlechin merged 5 commits into
mainfrom
refactor/landing-consistency
Aug 15, 2026
Merged

Make the site consistent with itself: one header rhythm, one copy of each component, one value per role#7
datlechin merged 5 commits into
mainfrom
refactor/landing-consistency

Conversation

@datlechin

Copy link
Copy Markdown
Member

A seven-lens audit across all ten pages, then the fixes. −219 lines net, and
every consistency counter that mattered is now zero.

The homepage had six passes of attention over six PRs. The other nine pages had
almost none, and that is exactly where the divergence had collected.

The header rhythm was hand-rolled forty-one times

Not one of the nine non-homepage pages used SectionShell. Compare and
DatabaseClient each built seventeen section headers by hand. Every copy had
drifted from the one the component documents:

  • The retired FullLine → h-4 → FullLine / FullLine → h-2 → FullLine pattern
    was still intact on six pages — four rules where the system draws two, which
    renders as a 1px rule, 16px gap, 1px rule band. SectionShell's own docblock
    calls that a mis-rendered table border and says it was deleted.
  • None of them imported AccentLine, so the orange tick that marks the
    start of every section on the homepage was missing from all forty-one.
  • Every label, heading and lede used pl-4 — flush against its rules with no
    vertical padding, and no right padding either, so long text ran into the
    container rail.
  • Three h1 size ramps and three padding treatments across the eight pages with
    an h1.

There is now one HeaderStack. SectionShell renders it with an h2 and a
landmark, PageHeader with an h1, SectionHeader alone for the two pages whose
sections live inside conditional fragments no component wrapper lines up with.
Duplicating a rhythm is how a rhythm stops being one.

Compare's Verdict section had an eyebrow and no heading at all. It has one.

Four things existed in two to eight copies

The Apple mark, eight copies in four shapes. Four carried no size class at
all — inside a flex button that means the browser guesses — and half were
missing aria-hidden, announcing an unlabelled graphic inside a button that
already reads "Download for Mac".

The tick, three copies that disagreed on colour. One used bare --primary
at 2.62:1, in the glyph that is the sole carrier of meaning in a comparison
column.

That is why availability is now one Availability component rather than a glyph
you are trusted to pair with text. Both glyphs are aria-hidden, so a cell
holding only one announces as empty — and an empty cell in a comparison table
does not read as "no answer", it reads as "not included". The pricing table
shipped that, was fixed in #3, and Compare lost the same fix midway through
this very branch
when its local wrappers were swapped out. A pair that must
not be separated should not be two things.

The FAQ, two presentations. An open grid on the homepage and /faq, a
<details> accordion on Compare and DatabaseClient — byte-identical between
those two, and invalid in both, since a <dl> may contain only <dt>, <dd>
and <div>. Open won: three to eight questions is nothing to collapse for, and
an answer behind a disclosure ranks lower and is never skimmed.

SectionBlock and Bullet, byte-identical in Privacy and Terms.

Images and tables

Four screenshot blocks fetched both themes. hidden dark:contents — and
display: none does not cancel an in-flight request, which is the exact reason
themed-image.tsx exists and says so in its docblock. Those pages also skipped
the WebP ladder that had already been generated for the same files.

Compare's comparison table was a grid of divs with no table semantics,
drawing its column separators at the row weight — in the one table where the
columns carry the entire meaning.

Three scrolling code blocks were unreachable by keyboard. A container that
scrolls but holds nothing focusable fails WCAG 2.1.1. The two that do hold
focusable children are fine and deliberately unchanged.

One value per role

before after
header spacer divs 22 0
pl-4 on an element 43 0
raw gray-950 / black / white alphas 12 0
transition-all 7 0
arbitrary text-[Npx] 4 0
disabled:opacity values 2 1
hand-written prose links 21 0
<details> accordions 2 0

The accent tint went from seven values in two syntaxes to two roles, written
down in app.css: /5 marks a surface as the featured one, /10 fills a chip
or a selected control. safety.tsx keeps its own finer ladder and that is
not drift — the tint there encodes how far up the six-level ladder a row sits,
so it has to be finer and evenly spaced.

The mobile nav also keeps its own close icon. It shares a path with the "not
included" cross and nothing else — 20px inheriting the button's colour versus
14px in muted-foreground — and sharing a path is not sharing a role.

Verification

typecheck, build and pint clean. 105 tests / 1849 assertions with
REQUIRE_SSR=1. Every page type re-rendered and checked: /, /terms,
/privacy, /refund-policy, /download, /faq, /blog, /compare/dbeaver
and /postgresql-client each show one <main>, one <h1>, the accent tick
present, zero doubled rules and zero accordions.

Nine pages built the section header by hand, forty-one times between them, and
every copy had drifted from the one `SectionShell` documents:

- The retired `FullLine → h-4 → FullLine` / `FullLine → h-2 → FullLine` pattern
  was still intact on six pages — four rules where the system draws two, which
  is a 1px rule, 16px gap, 1px rule band. SectionShell's own docblock calls that
  a mis-rendered table border and says it was deleted.
- None of them imported AccentLine, so the orange tick that marks the start of
  every section on the homepage was missing from all forty-one.
- Every label, heading and lede used `pl-4`, so they sat flush against their
  rules with no vertical padding — while the homepage gives them `py-3` and
  `py-4 sm:py-5`. `pl-4` also means no *right* padding, so long text ran into
  the container rail.
- Three h1 size ramps and three padding treatments across the eight pages that
  have an h1.

The fix is one `HeaderStack`, shared. `SectionShell` renders it with an h2 and a
landmark; `PageHeader` renders it with an h1; `SectionHeader` renders it alone
for the two pages whose sections live inside conditional fragments that no
component wrapper lines up with. Duplicating a rhythm is how a rhythm stops
being one, so there is now exactly one copy of it.

Compare's Verdict section had an eyebrow and no heading at all, unlike every
other section on that page. It gets one.

Twenty-two spacer divs are gone, including the last two in pricing, and `pl-4`
survives only in the comments explaining why it went.
Four pieces of markup existed in two to eight copies each, and every set had
drifted.

**The Apple mark, eight copies in four shapes.** Four of them carried no size
class at all, which inside a flex button means the browser guesses; half were
missing `aria-hidden`, so a screen reader announced an unlabelled graphic in the
middle of a button that already reads "Download for Mac".

**The tick, three copies that disagreed on colour.** Two used `--primary-strong`;
one used bare `--primary` at 2.62:1 — the failure `SectionLabel` was created to
stop, surviving in the one glyph that is the sole carrier of meaning in a
comparison column.

That last point is why availability is now a single `Availability` component
rather than a glyph you are trusted to pair with text. Both glyphs are
`aria-hidden`, so a cell holding only one announces as empty — and an empty cell
in a comparison table does not read as "no answer", it reads as "not included".
The pricing table shipped exactly that, was fixed, and Compare lost the same fix
partway through this very commit when its local wrappers were swapped out. A
pair that must not be separated should not be two things.

**The FAQ, presented two ways.** An open two-column grid on the homepage and
`/faq`, a `<details>` accordion on the comparison and database pages — the two
accordions byte-identical, and both invalid, since a `<dl>` may contain only
`<dt>`, `<dd>` and `<div>`. Open won: these pages carry three to eight
questions, so there is nothing to collapse for, and an answer behind a
disclosure is one a search engine ranks lower and a skimming reader never sees.

**`SectionBlock` and `Bullet`, byte-identical in Privacy and Terms.** Two places
for one rule to drift, and two places to fix it when it did.

The mobile nav keeps its own close icon. It shares a path with the "not
included" cross and nothing else — 20px inheriting the button's colour versus
14px in muted-foreground — and sharing a path is not sharing a role.
**Four screenshot blocks fetched every variant.** Compare and DatabaseClient
drew their product shots as a pair of `<picture>` elements toggled with
`hidden dark:contents`. `display: none` does not cancel an in-flight image
request, so every visitor downloaded both the light and the dark file —
`themed-image.tsx`'s own docblock says this is why it exists and why it selects
on `prefers-color-scheme` instead. Those pages also skipped the WebP ladder
that had already been generated for the same screenshots, so they shipped raw
PNG where the homepage ships WebP.

**Compare's comparison table was a grid of divs.** No table semantics, so
nothing tied a cell to its column, and it drew its column separators at the row
weight — in the one table where the columns carry the entire meaning. It is now
a `DataTable`, matching the plan comparison, and its scroll container is
reachable by keyboard.

**Three scrolling code blocks could not be reached at all.** A container that
scrolls but holds nothing focusable is unreachable from the keyboard, which is
WCAG 2.1.1. The two scroll containers that *do* hold focusable children — the
billing toggle and the category rail — are fine as they are and deliberately
stay that way.

Inline code had five treatments, one of them still filling with
`bg-black/5 dark:bg-white/10` after the rule migration had removed every other
raw alpha pair on the site. It has one now, and `rounded-key` is the radius
token that exists for exactly this size of thing.
**Raw alpha pairs are gone.** Twelve survived the two-weight rule migration, in
five roles: two inset panel fills written as `gray-950/[2.5%]` and
`gray-950/[0.02]`, two placeholder-tile hover borders that disagreed with each
other in dark mode (`white/20` against `white/25` for the same tile in two
files), and two blog row hovers hand-rolled in a neutral grey. The panels take
`--surface-raised`, which is the same "one step off the base ground" the raised
section tone uses and reads the same way from the other side. The tiles take the
accent, like every other hover on the site. The blog rows take `[data-row]`,
which is what it is for, and get focus parity as a side effect — they had none.

**`transition-all` is gone.** Seven elements animated every animatable property
to change two: a dropdown wanted opacity and transform, three logo marks wanted
opacity and filter, and the pricing CTAs wanted opacity. Each now names what it
animates and takes the motion tokens, which had almost no consumers.

**Disabled is one value.** It was 40% in two places and 50% in two others.

**The tint scale is written down.** Seven values in two syntaxes collapse to two
roles — `/5` marks a surface as the featured one, `/10` fills a chip or a
selected control — with the reasoning recorded in app.css. `safety.tsx` keeps
its own finer ladder, and that is not drift: the tint there encodes how far up
the six-level ladder a row sits, so it has to be finer and evenly spaced.

**The prose link is one class list.** Nineteen call sites across six files
wrote it out by hand, and two more variants existed that were underlined but not
coloured, or coloured but not underlined.
One of the twenty-one carried extra layout classes, so the exact-string sweep
skipped it. It composes now instead of restating.
@datlechin
datlechin merged commit 31ec438 into main Aug 15, 2026
5 checks passed
@datlechin
datlechin deleted the refactor/landing-consistency branch August 15, 2026 04:02
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