MEtronome · 1.1 DESIGN LAB · 2026-08-30 · FACE SWITCHER

FACE LAB

One metronome, three faces — the beat does not notice you switching.
The classic beat-dot face is the default, forever; everything else is an alternate face of the same engine, the way a watch face is a rendering of one movement. So this page is not three mockups side by side. It is one stage, one clock, one truth, and a switcher. Change the face mid-bar and the bar keeps going — that is the proof, and the counters under the stage are there so you can check it rather than take my word for it.
VISUAL ONLY · SILENT BY DESIGN — the beat clock drives pixels, not audio. No tap-to-arm needed.
120 BPM
 
H204°
FACE — the app's own theme-card picker, borrowed (SettingsView.swift:452-482 / ThemeCardSwatch :492-538)
8:09▮▮▮
((•))Guest
≡ EDIT
−
120
BPM · MODERATO
+
001·01 BAR · BEAT
BAR1
STOP
TAP
TAP TEMPO120 BPM
TIME SIG4/4
SUBDIVISION♫
ME
SOLO
120
BPM · MODERATO
BAR001
BEAT01
METER4/4
NOTE ♫EIGHTH
−
TAP
+
ME
SOLO
120
MODERATO
001
STOP
MetronomeSessionPlaylistStatsMore

THE ONE CLOCK — the continuity proof

Everything each face draws is a function of one monotone number. If switching faces restarted anything, BEATS SINCE ARM would jump and CLOCK REBASES would climb. Switch faces mid-bar and watch neither move.

And check that the needle can move at all. A counter wired to code that cannot increment it proves nothing, so the console carries its own negative control: set BAD FACE · RE-ARMS and switch. That branch does exactly what a face with an .onAppear { engine.restart() } would do — the row turns red, the ledger says CLOCK RESTARTED, and the bar visibly snaps back to 1. Set it back to CLEAN SWITCH and the counter stops climbing. That is the proof; the zero on its own was a thermometer with no mercury.

BEATS SINCE ARM0.00
t0 (armed at)—
FACE SWITCHES0
CLOCK REBASES ON SWITCH0
EPOCH REBASES (bar-line tempo/meter)0
BAR · BEAT · SUB1 · 1 · 1

SWITCH LEDGER

— no switches yet —

VERTICAL BUDGET — measured, not asserted

852 pt screen − 59 pt safe area − 83 pt tab bar = 710 pt. SPENT is the sum of the active face's non-spacer children and their margins, read from offsetHeight — a layout value, which a CSS transform does not affect, so this is true points at any stage scale. The spacer's own measured height is printed beside the slack as an independent cross-check; the two agree within integer rounding. Under 710 means the SE-class latent scroll (MetronomeView.swift:106-122) never actually fires.

Measured, all four states fit: CLASSIC 558 at 4/4 · RADIAL 681 · RADIAL + MAP 683 · STAGE 517. Note what that ranking says — the radial face is the tight one either way, and D1's published estimate of "~42 pt slack" does not survive adding a session pill. Dynamic Type on the gauge captions has to be capped or the dial shrinks first.

Move the METER and watch CLASSIC's number move. It did not, until 2026-08-30: this face drew every meter as one row, so it read 558 pt at 3, 4, 5, 6 and 7 alike — insensitive to the one variable that has historically broken this layout. The shipping row splits above six beats (LEDBeatRow.swift:66), so 7/4 now costs a second row — 625 pt, 85 pt of slack — and 6/8 draws the compound group divider instead of hairlines (still 558: an absolutely-positioned divider is free). A budget that cannot go up is not a budget.

ACTIVE FACECLASSIC
SPENT— pt
SLACK— pt

ACCENT FAMILY — computed both ways

DERIVED solves each token's lightness by bisection until its WCAG ratio against #0C0C0E equals the ratio the shipping Ember hex actually measures — the targets are read off Theme.swift:123-139 at load, never typed.

Same SOLVER as accent-lab.html:1205-1227; not the same constants, and saying "same law" was too loose. Shared: bisect lightness at a fixed saturation until the ratio against the ground hits a target. Different: this page reads its targets off Ember rather than typing them, and it picks its own per-role saturations — six of nine differ from accent-lab's bright-field set (ink 8→42, dimText 30→60, ghost 12→60, borderDot 32→8, lit 72→85, deep 68→76). The one that could have mattered is burnt: accent-lab's Studio family anchors underlay to the armed colour (accent-lab.html:1214), which this page does not — but its Console family, the dark one, solves underlay against the ground exactly as here (accent-lab.html:1220, S 55 to this page's 60, target 3.00 against Ember's measured 3.03). On this field the two agree in shape and very nearly in number.

ARM-STEP is the luminance jump from resting accent to armed — the thing that makes a beat read as an onset rather than a hue. TIER SPLIT is ember-vs-cream: if it collapses, the once-per-bar law loses its second tier.

The two ΔE rows are the only ones that can fail, and that is the point. Every WCAG row above is algebraically pinned — each token is solved to its own ratio against the same ground, so ratio(lit,amber) ≡ ratio(lit,bg)/ratio(amber,bg) and the ACTIVE column reproduces the EMBER column at every hue on the wheel. What the solver does not preserve is chromatic separation: two tokens can both hit their target luminance and still slide onto each other in colour. The radial face reads by exactly that — played versus unplayed is a burnt/cold area code, and the once-per-bar law's second tier is lit/ink. Both are measured in ΔE*ab, and their floor is 80 % of what Ember itself achieves for the same pair, so the floor is derived too. Drag the hue slider: the ΔE rows move and can go under; the seven above them will not budge.

AND THE DEFAULT HUE IS UNDER BOTH FLOORS — DELIBERATELY, BECAUSE THAT IS THE FINDING. Swept all 360 hues on this page: Ember measures 42.2 area and 45.9 tier, and 217 of 360 hues clear 80 % of that. The ones that do not are a single contiguous arc — H 161°–257°, cyan through blue into indigo — bottoming at 26.9 at H 185° (area, −36 %) and 29.9 at H 181° (tier, −35 %). That arc is where accent-lab.html's recommendation lives. The luminance law says Cyan is legal and it is right; this says a cyan family will read the played-vs-unplayed ring and the ember-vs-cream tier noticeably flatter than Ember does, and that cost is invisible to every contrast row on this table. It is not a veto — it is a number to price, and it is exactly the sort of thing an instrument with a pinned needle would never have shown.

R1 GEOMETRY — arc-minutes at the stand

θ′ = cappt ÷ (1.8667 × dm), from R1 §2.2's h ≈ d·θ·0.0002909 and §1's 163 pt/in. Cap ratio 0.700 for Bebas and Overpass Mono alike, measured from the bundled TTFs (glance-lab.html:1553-1557). R1 §7.1 puts the BPM numeral in the 20–22′ optimal band at its working distance; secondary text may sit at the 14–16′ floor.

Switch to CLASSIC and read the top row. Bebas 72 is a 50.4 pt cap — 27.0′ at 1 m and 13.5′ at 2 m. At a music stand the default face is comfortably readable; at 2 m it is under R1's 14′ automotive floor, never mind the 20–22′ band its own category calls for. That single number is the entire argument for alternate faces — not that CLASSIC is wrong, but that it is a 1 m instrument, and STAGE (24.7′ at 2 m) is the same metronome answering a different room.

WHAT THE RADIAL FACE ADDS TO D1. glance-lab's D1 is a display; a face has to be operable. Three additions, each with its cost stated:

· Transport that respects the ring. D1 has no tempo control anywhere — the numeral inside the ring is a readout with nothing to drive it. This adds one 74 pt bar with three zones (− / TAP / +) beside a 74 pt ignition. All zones clear the 44 pt touch floor. The ignition is 74, not CLASSIC's 92, because the ring already carries "alive"; two 92 pt bloom sources would double the sacred hex on one screen.

· Song Map coexistence. The approved three-band display (songmap-lab.html:476-527) compressed to sit under a dial. Costs, stated: the dial drops from 344 to 252 pt (ring Ø 318 → 232, pips Ø 240 → 182), the BPM numeral from 118 to 106 (cap 74.2 pt — 19.9′ at 2 m against the full face's 22.1′, i.e. the bottom edge of R1 §7.1's 20-22′ optimal band rather than inside it), the BEAT gauge is dropped (the ring shows beat at 100 % duty and the plate shows section position), the "BARS IN SECTION" caption is displaced by the global bar count, and the tempo NAME loses its seat entirely — it is the only readout on the face that is a description rather than a measurement, and a map whose sections carry their own tempos arguably should not show one global name anyway. The NEXT gutter falls to Bebas 34 — 23.8 pt cap, 12.7′ at 1 m, below R1's 14′ label floor; it clears 16′ only at 0.7 m (18.2′). So with the map on, the NEXT band is a 0.7 m object. That is the honest price and the owner should price it.

· Session pill — four states where the shipping one has two. The flagship is already on the performance face: MetronomeView.swift:180 renders a pill whenever session.role != .none, and CLASSIC on this page now draws that exact object (MetronomeView.swift:792-811) rather than pretending the slot is empty — an earlier cut of this page omitted it, which made switching away from a live session look like the session had been destroyed. What the radial pill adds is vocabulary: today's has a host state and a guest state and no way at all to say RECONNECTING, because role stays .guest right through a reconnect. Set the console to RECONNECT and switch between faces to see it: the radial pill dims and pulses on the bar phase; the shipping pill reads a steady, confident "Guest" while the link is flapping. That phase is legible today only as a row in a Settings list (FollowingHostBanner.swift:34-43) — nowhere you can see it while playing. Geometry is constant across SOLO / HOSTING / FOLLOWING / RECONNECTING, so joining a session is a luminance onset (R1 §5.2's top-ranked peripheral channel), not a reflow. The peer-count string and the SF-Symbol stand-in glyphs are invented for the mock [unverified].

WHAT STAGE GIVES UP, ON PURPOSE. No subdivision channel, no gauges, no map. At 2 m in the corner of the eye a 240 pt pip ring running at 8 Hz is texture, not information (R1 §5.1: acuity collapses fast with eccentricity). What is left is a 334 pt ring, the largest numeral on the page, a bar figure and an ignition — 193 pt of measured slack, spent on air rather than on readouts, which is the whole reason it reads from across a room. Its numeral clears 24.7′ at 2 m, above R1's optimal band; CLASSIC's clears 13.5′.

WHAT THIS PAGE DOES NOT SETTLE. Whether faces are per-user or per-context (a Song Map probably wants RADIAL; a live gig probably wants STAGE); whether the switcher lives in Settings beside the theme cards or as a long-press on the numeral; and the lit-law tension named in the header — the beat channel fires the ember once per bar, but the sweep, filament and ignition are continuously lit. All three are owner calls, not research questions. One new one, opened by this round: CLASSIC now shows the shipping two-state pill and the other two faces show a four-state one, which is honest but is also two capsules for one idea. Either the shipping pill grows the two missing phases and every face wears it, or the new pill lands everywhere and CLASSIC stops being "today's screen, unchanged". That is a call, and it is cheap either way.

Sources touched: Theme.swift:123-139 · LEDBeatRow.swift:66, 98, 99-105, 117-121, 140-159, 292-294, 324-336, 338-347 · TransportControls.swift:6-10, 85-88, 291-293 · MetronomeView.swift:106-125, 175-183, 376-384, 792-811 · SettingsView.swift:452-538 · ModelsMetronomeState.swift:29-43 · FollowingHostBanner.swift:27-56 · glance-lab.html (D1) · songmap-lab.html (three-band display, SONG at :894-900) · accent-lab.html:1205-1227 (deriveFamily) · R1-glanceability.md §1, §2.2, §2.3, §5.1, §5.2, §7.