Decisions

The parts of the system that look like mistakes, and the measurements that made them the right answer.

Each entry is a record: what was decided, the number that decided it, and the date where the record has one. An entry with no date is from the first drawing. A decision that runs against a good instinct — a border that fails a guideline on purpose, a ladder that is not symmetric — is written here so the next person finds the number before they find the instinct.

Light and dark are not symmetric

In light, surface/raised and surface/overlay are both white; a shadow separates a card from a modal. In dark, shadows stop reading as elevation, so overlay has to be a lighter colour step instead. The two modes describe the same hierarchy through different means, and flattening that into symmetry breaks the dark theme.

Button labels are themed, not constant

In dark the accent fill lightens across hover and pressed, while its label darkens to compensate. This is the trap that makes the palette look incapable of a lighter dark-mode hover: hold the label at white and the hover step lands at 1.77:1, nowhere near AA, with no lighter violet available that fixes it. The label was never the constant.

on-accent over accent, hover, pressed — dark: 7.73AAA 11.02AAA 13.99AAA

The highlight button lightens on hover, in both modes

interactive/tertiary resolves to flare, the gold the peaks take before they turn pink. Its fill is flare/400 with a dark label. The step below it, flare/500, is the stop where no label passes at all.

Rejected: flare/500 as the hover fill.

with stone/900 — 4.293:1 · with white — 3.803:1

So the only three-step ladder that passes goes the other way: 400, then 300, then 200, the label staying dark throughout. Light and dark share it, because the rule that produced it is the same in both — the accent already lightens in dark for the same reason. The tone is withheld from the outline and ghost variants, enforced in the type signature: the theme has no flare text colour, and nobody has drawn the button that would need one.

Checkboxes take the strong border; text fields have none at rest

Decided 2026-09-07; the text field corrected 2026-09-24 from the drawing

border/strong is the same primitive in both modes, which is unusual. It is the only value in the ramp clearing 3:1 against all four surfaces in light and dark, so a checkbox and a radio — where the border is the control — use it without a per-surface exception. Sitting mid-ramp, it contrasts in both directions.

The text field is the exception. The decision of 2026-09-07 kept a hairline in border/default at rest, 1.40:1 in light and 1.53:1 in dark, failing 1.4.11 to preserve the drawn look, and this entry called it unreconciled with the code, which draws no resting border. The drawn field has no border at rest, and a hairline on hover and on focus, so at rest the code is the drawn look and the 2026-09-07 note described neither (corrected 2026-09-24, fidelity audit). At rest the boundary is the fill, 1.07:1 against a card in light, and the Input page records that as a known gap.

The dark ramp holds three elevation levels, and sunken shares the canvas

Decided 2026-09-11, with the deep tail

Base, raised, overlay — 950, 925, 900 — and then it is full. The ramp ends at 950, so in dark surface/sunken is the canvas: a well reads as recessed inside a card, and on the canvas it takes a border. A dropdown opened inside a modal stays on surface/overlay and is separated by a border rather than another fill step. A twentieth step was measured and refused — adjacent steps of the neutral it would have extended were 1.08 to 1.23:1 apart, which is how a system ends up with two text levels nobody can tell apart. Running out is not a flaw to design around; it is a constraint to state plainly so nobody invents a level that collides with something.

925 is a surface step, and the only half step there will be

Decided 2026-09-12

The ladder used to be 950, 900, 800: one whole stop per level, ΔL .085 in OKLCH, and it looked strong because it was. Every reference system measured — Radix, Atlassian, Spectrum, Geist — places adjacent surface levels at .025 to .045. The rule against half steps was written against text levels nobody could tell apart, and no text is ever set in one surface against another, so one stop was added for surfaces and nothing else: 925 at L .205, in night and stone only, the two families a surface ladder is built from. The steps are now 0.043 and 0.043. The 975 the old rule refused stays refused. See Elevation and states for the survey.

Surfaces are measured in lightness, not in the contrast ratio

Decided 2026-09-12

The WCAG ratio adds 0.05 to both luminances, which flattens the dark end: Radix’s first two dark greys are 1.06:1 apart and everyone sees the step. This system’s new step is 1.08:1, and the suite’s old floor of 1.09 would have refused it while passing the jump that looked wrong. Text on a surface and a boundary on a surface stay on the ratio, which is what WCAG asks of them. A ladder step, a well inside a card and a wash over a surface are measured in OKLCH lightness, floor .035.

Hover is a wash, not a fill

Decided 2026-09-12

A row, a menu item, a ghost button and the neutral button all used to hover to one opaque colour: mist/100 in light, stone/700 in dark. Over a dark card that was ΔL +.184 where the references sit at +.05 to +.09, and in light it was invisible on surface/sunken, the same primitive. Now they take interactive/wash-hover, mist/500 at 8% in light and 12% in dark — one ink that darkens a light surface and lightens a dark one, laid over whatever is beneath. A hovered row on a dark card is 0.057 above it. The text beneath keeps its own token and is measured there: the tightest pair where rows live, text/tertiary on a hovered row on a light card, is 4.95AA. The filled ladders — accent, danger, success, tertiary — are untouched: a filled button changes its own colour, and its label is themed with it.

The menu takes a border in dark and not in light

Not an oversight of symmetry. Against the ground it falls on, the shadow reaches 1.19:1 in light at 8% opacity and 1.05:1 in dark at 64% — in dark it has stopped carrying elevation, whatever it is set to. The border is what separates the menu there, and against the canvas it is 3.55:1 in dark against 1.60:1 in light: stronger where it has to be. In light the shadow already does the work, and drawing the edge twice would look like a mistake, because it would be one.

Each menu tone hovers to its own fill

The drawing gives every row the same hover fill. Two measurements broke that. The drawn fill is surface/base, which in dark is darker than the menu — the row under the pointer would open a hole rather than light up. And the accent row’s label on a shared neutral fill was 3.50:1 in dark when the rule was made. The deeper dark tail brought it to 4.59:1, which passes, and the rule stays: a tone hovers to its own subtle surface because that is the design, not because of 0.09 of headroom. On its own surface the label is 9.64:1, and on the wash the neutral row takes now it would be 7.86:1.

The menu takes the top layer, and the suite cannot see it

Decided 2026-09-10, checked by hand in Chrome and Safari

jsdom, which runs the test suite, implements none of the popover API. The menu uses it anyway. A menu opens from inside other components — a row of actions in the Table, whose scroll container clips anything positioned inside it — and escaping that clip is the reason for an overlay to exist. The suite stubs the calls the component makes and asserts everything the component decides: roles, rows, keys, disabled rows, the stylesheet. Esc, the outside click, focus return and placement belong to the browser. All four were checked in Chrome, and this entry says so rather than letting a stub claim them.

No z-index tokens

Decided 2026-09-24

A z-index scale orders layers that compete on one stack, and nothing here competes. What floats goes to the top layer — the Dialog, Drawer, Popover, Tooltip, Select, Combobox, DatePicker, DropdownMenu, PageSize, Toast, the SideNav’s sheet and, through the Dialog, the CommandPalette: twelve components, no z-index, stacked in the order they opened. What stacks inside a component is shut in its own isolation: isolate, for example: the Scheduler’s sticky head at 3, the Table’s header at 1, the Card’s actions at 1, the Slider’s upper thumb input at 1, the thumbs of the SegmentedControl and Tabs. A z-index lives only inside a component that isolates, never above 3, and a test holds both. So the page’s stack is the product’s: a sticky bar at z-index: 1 sits over everything Alpenglow draws. The Scheduler and the Slider did not isolate until 2026-09-24, and a product’s sticky bar at 1 or 2 would have had the Scheduler’s head scroll over it. A third-party overlay with a high z-index still sits under an open <dialog>, which is the behaviour wanted.

The section list joins the prose at 2xl, and not before

Decided 2026-09-12, moved on 2026-09-14 and 2026-09-24

The site’s page has three columns on a wide screen — the sections, the prose, the measurements — and a Table specimen needs 704px of prose to keep its header: 654 for the table and a specimen’s padding and hairline on both sides. Measured, that holds from 1500 of viewport: the 80px rail, the 232px drawer and two 40px margins leave 1108 of page between the margins, and the 152px list, the 168px measurements, three 20px gaps and a 24px spacer leave the 704 from that. It was a breakpoint of its own, 1496, until the breakpoints became a scale; now it is the scale’s step on the safe side of the measure, 2xl, 1536, and the test holds the inequality rather than the number.

Below 2xl the drawer waits under the rail

Decided 2026-09-15, moved on 2026-09-24

From 2xl down to the narrow bar at md, the drawer is out of the flow: the rail alone is the sidebar, the page gains the drawer’s 232, and the drawer slides out over the page — 140ms, the travel duration — while the pointer or the focus is in the sidebar, and back under the rail when they leave. Focus opens it because a keyboard reaches the drawer’s links through the rail’s; the search’s own dialog does not, since its field is focus inside the sidebar and the drawer would slide out behind the scrim. Hidden as well as moved, so its links are out of the tab order while it is under the rail. The reader without a pointer, on a tablet in this range, has the section’s page one tap away on the rail, and it lists the same pages as cards. The brand goes with the drawer on this tier; the rail is 80 wide and the name does not fit it. It started at 1440; one edge at 2xl now serves this tier and the wide page.

The Dialog’s title wraps

Decided 2026-09-13

It used to truncate with an ellipsis, and at xs the header’s grid left it 112px: two 40px buttons, two 40px gaps, and 48 of padding out of 320. Reschedule appointment, this system’s own example, read “Reschedule appoint…”, and the question a confirmation asks was cut at “Cancel th…”. A title the reader cannot read is a dialog they cannot answer, so the title wraps, balanced, and the gap came down to 24 — the header’s own padding — because “appointment?” alone is 115px. A stylesheet test refuses the ellipsis from now on.

A theme flip holds every transition but the toggle’s own

Decided 2026-09-12

A flip changes the colour of nearly everything at once, and every colour transition on the page fired together: measured on the Button page, 92 on the buttons and 7 on the section list, so the buttons faded for 120ms while the page around them snapped. The toggle now puts an attribute on the root for the frame the theme is written in, the stylesheet turns transitions off under it, and the next frame takes it away. The toggle’s own transitions are excepted, because the knob’s slide is the control answering the click. Measured after: four transitions, all the toggle’s.

The search index is built from the pages, not written beside them

Decided 2026-09-14

The palette searches pages, sections, tokens and props. The entries are read from the pages themselves, rendered to static markup at build time and walked: a page is searchable the day it is in the contents, a heading the day the page anchors it, a token the day it is in the theme. The file is a build artefact and is not committed — prose changes in nearly every commit, and a diff guard like the stylesheets’ would fail on most of them for nothing. The matcher is a prefix with one typo allowed from four characters, weighted so a page title outranks a token, a token a section, a section the labels and the labels the body, where a term counts for more the more it is said and the shorter the section; twenty-three pages do not need an engine, and each rule is a test.

The search suggests; it does not complete

Decided 2026-09-14

The Material 3 site fills its field with the first suggestion as you type. This one lists the suggestions and leaves the field alone: editing a field’s value under the reader’s fingers is the class of bug the date mask fought — an IME composition, Android’s Unidentified keys, a paste — and the list gives the same answer without it. Nor does it rank by popularity: the site is static and has no analytics at runtime, so the five suggestions offered before anything is typed are chosen by hand, after the reader’s own recent destinations.

A code block is a card, and its colour is the theme’s

Decided 2026-09-14

The site’s code sat on surface/sunken, which in dark is the canvas: the block had no edge at all. It now takes the Card’s surface — raised, a hairline of border/subtle, the 2xl radius — with the file’s name over it and numbered lines when the code is a file, and a button that copies it. The syntax is coloured by a tokenizer of the site’s own, five kinds painted with text tokens the suite measures on raised: a keyword in text/accent, a string in text/success, a comment in text/tertiary, punctuation in text/secondary. A grammar engine was not taken: its themes are fixed hex, and this site has no colour outside the tokens. The package-manager tabs the shadcn site draws over an install line are left out too: there is one line, and it is npm.