diff --git a/src/components/insights/sleep-stage-stacked-bar.tsx b/src/components/insights/sleep-stage-stacked-bar.tsx index fc2569782..15e50bb0b 100644 --- a/src/components/insights/sleep-stage-stacked-bar.tsx +++ b/src/components/insights/sleep-stage-stacked-bar.tsx @@ -106,6 +106,36 @@ export const STAGE_ORDER = ["DEEP", "REM", "CORE", "ASLEEP", "AWAKE"] as const; */ const NAP_KEY = "NAP"; +/** + * The nap band is drawn with a card-coloured outline, and that outline is + * load-bearing rather than decoration. + * + * No flat fill can carry this on its own. To clear the 3:1 that WCAG 1.4.11 + * asks between adjacent graphical objects, a nap colour would have to beat + * all five stage colours AND the card it sits on. In the dark theme the five + * stages span luminance 0.385–0.890, which forces the nap below 0.095, while + * clearing the card (0.014) forces it above 0.143 — no value satisfies both. + * The light theme is the same bind mirrored: the stages sit at 0.088–0.161, + * forcing the nap above 0.583, while the card (0.966) forces it below 0.289. + * Both are empty ranges, so this is arithmetic, not a shortage of tokens. + * + * A separator is the sanctioned way out, and it measures comfortably: + * + * stroke vs DEEP REM CORE ASLEEP AWAKE nap fill + * dark 6.78 6.86 11.82 11.92 14.65 9.60 + * light 6.04 7.37 5.80 5.90 4.81 5.60 + * + * Lowest pair 4.81:1, against a light-theme wake bout — the neighbour the + * nap most often lands on, since AWAKE is the top of the stage stack. + * + * The stage tokens themselves are untouched; they are shared with the + * hypnogram and the charts-visual-identity rule keeps them put. + */ +const NAP_SEPARATOR = { + stroke: "var(--card)", + strokeWidth: 2, +} as const; + /** * Dracula stage palette. Exported so the last-night hypnogram * (`sleep-hypnogram.tsx`) reuses the exact same tokens — per the @@ -436,9 +466,18 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) { dataKey={NAP_KEY} stackId="stages" fill={STAGE_COLORS[NAP_KEY]} + // The outline is what separates the nap from the stage + // beneath it; the two fills alone measure as low as + // 1.04:1. See NAP_SEPARATOR. + stroke={NAP_SEPARATOR.stroke} + strokeWidth={NAP_SEPARATOR.strokeWidth} isAnimationActive={false} > - + )}