engine/guardrails library

Theme guardrails (spec §5.4; docs/m3-theme-engine-design.md "Rules"; research theme-engine.md §4, §6.2, §12): validate a ThemeSpec on its final quantised ARGB values, and repair failures by OKLCH lightness bisection.

Rules (ratios are never rounded; 4.499:1 fails 4.5:1):

rule measured against floor (accessible)
digit / operator / = / function labels their face 4.5 (7)
display text, error text every display backdrop 4.5 (7)
preview text every display backdrop 3 (4.5)
caret every display backdrop 3 (4.5)
focus ring every background sample 3 (3)
operator / = vs digit faces face contrast ≥ 1.5 and min ΔEOK over {normal, protan, deutan, tritan} ≥ 0.08, unless the spec records a non-colour cue
= vs operator faces min ΔEOK (normal + CVD) ≥ 0.05
LCD ghost segments (Retro) the LCD panel at most 1.3 (1.3)
soft-key rims (Neumorphism), 1–1.5 dp every background sample 3 (3)
soft-key pressed label (the accent) every surface-faced key 4.5 (7)
outlined-key outlines (Neo-brutalism), 2–4 dp every background sample 3 (3)
outlined-key hard shadow: opaque, no blur, offset = press depth 3–5 dp
glass (Liquid Glass) labels on each tint composited over every mesh hull sample (meshHullSamples: stops, pairwise ¼ / ½ / ¾ points, three-way centroids) the darkest and lightest composites 4.5 (7)
glass display text on the backing panel over every hull sample (the bare mesh is never a display backdrop) 4.5 (7)
glass labels under the peak press glow every hull sample 3 (3)
glass operator / = tint vs digit tint over the same sample min ΔEOK (normal + CVD) ≥ 0.08, plus the weight cue
glass rims under the Accessible lock (opaque, ≥ 1 dp) every hull sample 3 (3)
clay drop shadows (Claymorphism): alpha 0x50–0xD8, chroma ≥ min(0.040, 0.75 × maxChroma) (night ≥ 0.035), within 40° of the key's hue (night: the shadow-hue reference), composite ≤ 0.85 × the body's luminance the scene base
clay keys vs the body: light digits ΔEOK ≥ 0.045 (≥ 0.025 vs the top wash), light function keys ΔEOK ≥ 0.045, colour-blind ΔEOK ≥ 0.03 and ≥ 0.03 darker, night keys ≥ 0.15 L above the scene base and spheres
clay highlight lift on every face and the pillow panel ≥ 0.025 L
clay function labels on their inner shade (dark labels) or highlight (light labels) 4.5 (7)
clay focus ring where it crosses each key group's drop shadow (0.65 × its alpha; focusRing "over key shadow") the base, top wash, sphere B and shadow core 3 (3)
clay display panel: an opaque pillow, lighter than the body and ΔEOK ≥ 0.025 from it
clay style: no outline, shadow offset = press depth 4–6 dp, blur 10–14 dp, shade 0.08–0.11, white highlight 0xA0–0xC8, nine-colour claySoft scene
Y2K Aero labels on every colour of the label zone (rest and pressed gel / metal profile, y2kLabelZoneSamples) the darkest zone colour 4.5 (7)
Y2K Aero labels darker than the face and pressed face; faces ≥ L 0.735 (0.755)
Y2K Aero renderer contract: white highlight, cap / caustic / depth / rim / shadow ranges, no state layer
Y2K Aero scene: kind, six colours, overlay alpha caps, dots pattern
Y2K Aero display panel opaque, L ≥ 0.85 or ≤ 0.24 (text is checked on the panel and its gloss)
Y2K Aero focus ring, and rims under the Accessible lock every scene sample, plain and under the key shadow 3 (3)
Y2K Aero operator / = vs digit faces contrast ≥ 1.2 or min ΔEOK (normal + CVD) ≥ 0.06, plus the weight cue
Y2K Aero light skies: digit and function faces (every face in a meadow) vs the ground band ΔEOK ≥ 0.08
paper outlines (Paper & ink), 1.25–1.75 dp every sheet sample (paper, grain, rules) 3 (3)
paper operator hatch at its opacity (0.60–0.75), plus the weight cue the operator face 3 (3)
paper = stamp the digit slips 3 (3)
paper labels under the ink wash's tide line (alpha ≤ 58) their face 4.5 (7)
cyber (Cyberpunk) operator cue: plates opaque, L ≤ 0.34, lighter labels; slabs opaque, L ≥ 0.64, darker labels, ≥ 3:1 against the digit plate; operator weight ≥ key + 200
cyber plate edge lines (opaque, 1–1.5 dp) their face 3 (3)
cyber key boundary: each group's face or opaque edge every scene sample 3 (3)
cyber scene: static cyberGrid, five colours, ink alphas ≤ 24 / 72 / 56, tilt ≤ 20°, panel scanlines ≤ 48 (0 accessible), cut keys 12–24 %, no state layer
Vaporwave weight cue: operators ≥ 200 heavier
Vaporwave operator / = vs digit faces (normal vision) min ΔEOK ≥ 0.08
Vaporwave accents vs digit and function faces min ΔEOK (normal + CVD) ≥ 0.06 and contrast ≥ 1.2
Vaporwave scene: ten opaque colours, horizon 380–460 ‰, drift 0 or ≥ 40 s, every sample Y ≤ 0.26 (dark) / ≥ 0.17 (light)
Vaporwave panel, title bar and ink opaque
Vaporwave key edges = the ink; light themes: the ink every scene sample 3 (3)
Vaporwave dark themes: each key's face or ink every plain floor sample a key can sit over 3 (3)
Vaporwave dark themes: each key's face, ink or outer bevel highlight (so the highlighted top and left edges) the grid at alpha k/8 over every floor sample and the horizon haze 3 (3)
Bauhaus shape cue: digit block or disc, operator the other, = a disc / fan / triangle unlike the digits, corners ≤ 4 %
Bauhaus primaries: operator and = two different primaries (C ≥ 0.08, hue windows red 18–40°, yellow 82–100° at L ≥ 0.78, blue 250–280°); neutral digits; function accent distinct (ΔEOK ≥ 0.10)
Bauhaus scene: its encoding, a dark field (L ≤ 0.22) the digits reach 3:1 on, digit, operator and = faces vs ground / field, scene shapes ΔEOK ≥ 0.10 from the ground 1.1 (1.1)
Bauhaus panel: display rule (shadow) on the sheet; sheet accent (highlight) min ΔEOK (normal + CVD) ≥ 0.07 the sheet 3 (3)

Display backdrops are the display surface, every background colour stop and, for patterned backgrounds, the pattern ink composited over each stop (the M2 layout draws the display straight on the background). A 7-segment LCD theme (TypeSpec.lcd, Retro) draws the whole display on an opaque LCD panel (the surface), so its only backdrop is the panel: digit segments are then checked against exactly what they sit on (≥ 4.5:1, 7:1 accessible), and unlit ghost segments must stay ≤ 1.3:1 against it so they never read as digits.

Classes

GuardrailLimits
Thresholds for one theme (standard or Accessible lock).
GuardrailViolation
A failed guardrail.

Enums

GuardrailRule
One guardrail.

Constants

brutalOutlineMaxCdp → const int
brutalOutlineMinCdp → const int
Lowest and highest outline width of outlined keys, dp × 100.
brutalShadowMaxCdp → const int
brutalShadowMinCdp → const int
Lowest and highest hard-shadow offset of outlined keys, dp × 100.
lcdGhostMaxRatio → const double
The ceiling for unlit LCD segments against the LCD panel.
softRimWidthMaxCdp → const int
softRimWidthMinCdp → const int
Lowest and highest soft-key rim width the guardrail accepts, dp × 100.

Functions

backgroundSamples(BackgroundSpec bg) → List<int>
Opaque background samples: every gradient stop and, with a pattern, the pattern ink composited over each stop.
displayBackdrops(ThemeSpec spec) → List<int>
Everything display text may be drawn on: the surface (composited over the background when translucent) plus backgroundSamples. A 7-segment LCD theme (TypeSpec.lcd) draws its display on the opaque LCD panel only, so the panel is its one backdrop.
faceSamples(int face, List<int> backgrounds) → List<int>
The opaque colours a key face is seen as (composited over each background sample when translucent).
lcdGhostColor(int panel, int segment, {double ratio = 1.15}) → int
The ghost-segment colour for an LCD panel lit by segment: the panel moved toward the segment colour in OKLab until it reaches ratio against the panel (default 1.15, comfortably inside lcdGhostMaxRatio). Pure and bit-exact (OKLab lerp + bisection).
passesGuardrails(ThemeSpec spec, {GuardrailLimits? limits}) → bool
Whether spec passes every guardrail.
repairKeyLabel(KeyColors k, ContrastPolicy policy, List<int> backgrounds) → KeyColors?
Re-solves k's label on its (opaque) face to policy; moves the face as well when the label alone cannot reach it. Null for translucent faces that fail.
repairTheme(ThemeSpec spec, {GuardrailLimits? limits}) → ThemeSpec?
Repairs every failing guardrail of spec (labels, display texts, then operator/= distinctness), and returns the repaired spec, or null if it still fails. A passing spec is returned unchanged.
separateFromDigit(KeyColors k, int digitFace, GuardrailLimits lim, {int? avoid, double avoidDeltaE = 0, int rounds = 6}) → KeyColors?
Moves k's face lightness away from digitFace in 0.06 steps (up to rounds) until it is distinct from it (and, if avoid is given, ΔE ≥ avoidDeltaE from that face too), re-solving the label each step.
solveOnAll(int argb, List<int> backdrops, double target) → int?
Moves the OKLCH lightness of argb (keeping chroma and hue) until it reaches target against every colour in backdrops; tries a neutral, then black/white. Null if unreachable.
validateTheme(ThemeSpec spec, {GuardrailLimits? limits}) → List<GuardrailViolation>
Every guardrail spec fails (empty = ships).