The Codebooks lens opens with its zone title 40px below the datum that Quotes, Sessions and Analysis all flush to. The cause is structural, not cosmetic — and the Library button is what put it there. Five ways out, drawn in the shipped design system. Built: C + D — toolbar item on desktop, demoted secondary in-pane on web, one flush full-width rule on both.
Both frames are the same geometry: 52px toolbar inset + 32px body pad = a content datum at 84px. Sessions puts its zone title there. Codebooks does not.
Sessions — the reference
Moderated by Martin Storey
Codebooks — today
Drag tags between groups to reorganise. Click a tag or title to rename it. Drop a tag on another to merge.
A is the pure alignment fix. B is your proposal. C is the toolbar. D and E move the button to somewhere the page already teaches.
A — flush the heading, leave the button where it is
Manual tags
Default
Drag tags between groups to reorganise. Click a tag or title to rename it. Drop a tag on another to merge.
<section> and the existing selector reaches it — no new rule at all.B — under the heading (your proposal)
Manual tags
Default
Drag tags between groups to reorganise. Click a tag or title to rename it. Drop a tag on another to merge.
C — the toolbar's empty contextual slot
Manual tags
Default
Drag tags between groups to reorganise. Click a tag or title to rename it. Drop a tag on another to merge.
ContentView.swift:2208–2229 already gives Quotes two contextual toolbar items
(starred filter, tag sidebar) and Analysis one (heatmap). Codebooks is the lens with an empty contextual slot — and a
loud in-pane button instead.MenuCommands.swift:1157), so a toolbar
item is the standard macOS promotion of an existing menu command — not an invention. books.vertical is already its symbol.ct() exists for —
show the in-page button on web, hide it on desktop — but it is a two-surface answer, not one.D — flush, and demote to secondary
Manual tags
Default
Drag tags between groups to reorganise. Click a tag or title to rename it. Drop a tag on another to merge.
CodebookSidebar.tsx:159).E — an add-card at the foot of the list
Manual tags
Default
Drag tags between groups to reorganise. Click a tag or title to rename it. Drop a tag on another to merge.
.new-group-placeholder ("+ New group") and the
library modal's own .tile-create. A dashed card meaning "one more of these" needs no explanation here.The rule under “Codebooks” stops short of the pane edge by exactly the button’s width. It is not a styling oversight: the h1 is pane-wide by declaration and column-wide by containing block.
Today — and option A/D as first drawn
Drag tags between groups to reorganise.
R3 — the atom becomes the row
Drag tags between groups to reorganise.
The atom sets max-width: none so the rule spans the pane — but the h1’s
containing block is <div style="flex:1;min-width:0">, the left column of the flex row.
“Full width” resolves to the width of that column, which is the pane minus the button. Every other lens draws
its rule pane-wide because its heading’s containing block is the pane.
So the wrapper causes both defects at once: the datum miss (heading is a grandchild, selector doesn’t reach) and the short rule (heading is column-width, not pane-width). Any fix that keeps a flex column around the heading keeps the second one.
.codebook-header and null it on the h1. Works, and it is
precisely the drift the atom was created to stop: Codebooks’ rule would be drawn by a different box than every
other lens’s, re-opening the “a class you have to remember” failure the atom’s own header comment describes..section-heading becomes a flex row, action inside the h1. Border spans the
pane by construction, one CSS change, and .framework-section-header on this very page is the precedent one
scale down. Cost: a <button> inside an <h1> is valid HTML but VoiceOver reads
“Codebooks Codebook Library, heading level one”. The h2-scale precedent avoids this only because its title is a plain div.<div class="section-heading"><h1>…</h1>{action}</div>.
The border sits on one box that spans the pane by construction; the button is a sibling of the heading, so the
heading announces as just “Codebooks”; the datum selector is untouched (it matches the class, not the tag); and the
static-render templates that write <h2 class="section-heading"> keep working, because the CSS never
requires an inner element. All 19 call sites are unchanged — SectionHeading.tsx absorbs the shape, which is the
entire reason that component exists.Worth noting the stakes: under C the desktop heading row is bare — the button is in the
toolbar — so the truncation only ever shows on the web SPA. That lowers the urgency, not the correctness. Doing R3 now
makes a zone-level action free on Quotes, Sessions and Analysis too, which is the same argument that made
SectionHeading a component rather than a class.
Shipped 20 Aug 2026. The numbers below are the ones the atom now guarantees.
.section-heading is now the row, not the text: a wrapper carrying the keyline,
with the <h1> and an optional action inside. The shape does not vary with the action —
every zone title in the app is the same box, so a change to the title treatment lands on all five
lenses identically, and a lens that grows an action later gets the geometry the others already have.
.codebook-header is deleted; it was the only per-lens header wrapper and it caused both defects.
| Measure | Value | Why |
|---|---|---|
| row height | 40.6px | the h1’s line box (28px × 1.45) — the 33.65px button fits inside it, so a row with an action is exactly as tall as a row without one and the keyline lands at the same y on every lens |
| button → keyline | 6.4px | align-items: flex-end lands the action’s bottom on the
title text’s, so both sit one padding-bottom above the rule — one bottom gutter, not two |
| row top → button | 6.95px | falls out of the above; puts the button’s optical centre within 0.5px of the capitals’ centre, with no magic number to maintain |
| title → action | 24px | gap: var(--bn-space-lg) — text cannot crash into the
action at any width or locale; min-width: 0 makes a long title wrap rather than push the action off |
| right edge | pane edge | the keyline and the action both end where the content margin does, level with “Codebook lab” in the framework header below |
No new exceptions. The action slot is the same construction as
.framework-section-actions one scale down (flex, centred, --bn-space-sm, never shrinks), so the three
title-left / action-right rows a lens stacks are one pattern at three sizes. The button is the stock .bn-btn atom,
demoted from .bn-btn-primary. Desktop reuses the existing Codes ▸ Browse Codebooks… action string,
symbol and help text; the only new locale key is the toolbar’s one-word label.
| Option | Datum fixed | Discoverability | Build | Surfaces |
|---|---|---|---|---|
| A flush only | yes | unchanged (loud) | 1 line | one |
| B under heading | yes, most cleanly | unchanged | markup only | one |
| C toolbar | yes | higher on desktop, unchanged on web | Swift + ct() gate | two |
| D flush + secondary | yes | lower | 1 line + class | one |
| E add-card | yes | lower until scrolled | new component | one |
Every option fixes the datum, because every option removes the flex wrapper or flushes past it. The alignment is therefore not the deciding question — it is free in all five. The deciding question is how loudly a browse affordance should speak on a page whose real work is silent (drag, rename, merge).
C + D compose. Toolbar item on desktop, quiet secondary in-pane on web, one flush
heading on both. That is the shape the codebase is already bent toward: an empty contextual toolbar slot on exactly this
lens, a native menu item already wired, and a ct() helper built for this fork.
The datum rule is a single selector in
templates/report.css:87:.center > main > section:first-of-type > .section-heading { margin-top: 0 }Sessions and Quotes render
<section>as the direct child of<main>, so it matches. Codebooks renders a fragment whose first child is<div class="codebook-header">— a flex row holding the h1 and the button. Two mismatches at once: not a<section>, and the heading is a grandchild. The selector misses, the atom'smargin: 2.5rem 0 1remstands, and the title drops 40px.This appeared on 6 Aug 2026, when Codebooks' h1 was given the
.section-headingclass it had always been missing (docs/design-lens-template.md§ "Systematising the heading"). Before that it had no class, no rule, and — by luck — no margin either. The 22 Jul audit measured Codebooks at 84px, dead on the datum. Fixing the missing rule handed it the missing margin.Because
align-items: flex-startputs the button at the top of the row while the h1's own margin pushes it down inside its flex item, the button now sits higher than the title it belongs to. That is what the eye is objecting to, and it is why "move the button" and "fix the alignment" feel like the same request.