# Gamma Desk > A pixel value is not an amount of light. sRGB stores brightness on a curve, so level 128 is > 21.6% of the light and 50% of the light is level 188 — 60 levels apart. > Blending is arithmetic on those numbers, so mixing black and white at 50% gives #808080 > if the stored values were interpolated and #bcbcbc if the light was. Both are > real pipelines — CSS does the first, SVG filters do the second — and nothing records > which a design meant. ## The one thing to know **A PIXEL VALUE IS NOT AN AMOUNT OF LIGHT.** Everything on this page follows from that. ```text sRGB -> light c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ^ 2.4 light -> sRGB l <= 0.0031308 ? 12.92 * l : 1.055 * l^(1/2.4) - 0.055 ``` Level 128 — the middle of the number range — is 21.6% of the light. And 50% of the light is level 188. 60 levels apart, 24% of the whole range. **Which matters because blending is arithmetic on those numbers.** Mix black and white at fifty per cent: interpolating the stored values gives #808080 at 21.4% of the light, and interpolating the light gives #bcbcbc at 50% of the light. 60 levels apart and 2.3× the light, from the same instruction. **BOTH ARE REAL PIPELINES.** CSS `opacity`, `rgba()` backgrounds and plain gradients interpolate the stored numbers, and so does nearly every image resize. SVG `filter` primitives default to `color-interpolation-filters: linearRGB`, and colour-managed compositors work in light. The same specification produces different pixels depending on which tool ran it, and **nothing in CSS, SVG or a design file records the choice.** **And WCAG contrast is defined on relative luminance — on the light.** A white panel at 18% over #1a1a1a gives #434343 one way and #787878 the other, and white text on it is 9.86:1 (AAA) against the first and 4.4:1 (AA large only) against the second. The accessibility verdict depends on a choice nothing records. ## The curve | Stored level | Of the light | A gamma-2.2 guess | How far out | | --- | --- | --- | --- | | 0 | **0% of the light** | 0% of the light | — | | 5 | **0.152% of the light** | 0.0175% of the light | 8.7× | | 10 | **0.304% of the light** | 0.0805% of the light | 3.8× | | 16 | **0.518% of the light** | 0.226% of the light | 2.3× | | 32 | **1.44% of the light** | 1.04% of the light | 1.4× | | 64 | **5.13% of the light** | 4.78% of the light | 1.1× | | 96 | **11.7% of the light** | 11.7% of the light | 1× | | 128 | **21.6% of the light** | 22% of the light | 1× | | 160 | **35.2% of the light** | 35.9% of the light | 1× | | 188 | **50.3% of the light** | 51.1% of the light | 1× | | 192 | **52.7% of the light** | 53.6% of the light | 1× | | 224 | **74.5% of the light** | 75.2% of the light | 1× | | 255 | **100% of the light** | 100% of the light | 1× | **Level 128 is 21.6% of the light, and 50% of the light is level 188.** 60 levels apart — 24% of the whole range — and nothing in the number says so. The third column is the OTHER wrong answer. Plenty of tools approximate sRGB as a plain gamma 2.2, which is close in the midtones and badly wrong near black: the real curve has a straight segment below level 10 and a power curve has none. At level 5 the approximation is 8.7× out, which is exactly where shadow banding lives. ## The two pipelines, side by side | Mix | Opacity | Numbers mixed | Light mixed | Levels apart | Times the light | | --- | --- | --- | --- | --- | --- | | black and white | 10% | #e6e6e6 (78.7% of the light) | #f3f3f3 (90% of the light) | **13** | 1.1× | | black and white | 25% | #bfbfbf (52.3% of the light) | #e1e1e1 (75% of the light) | **34** | 1.4× | | black and white | 50% | #808080 (21.4% of the light) | #bcbcbc (50% of the light) | **60** | 2.3× | | black and white | 75% | #404040 (5.09% of the light) | #898989 (25% of the light) | **73** | 4.9× | | black and white | 90% | #191919 (1% of the light) | #595959 (10% of the light) | **64** | 10× | | white on a dark photo | 50% | #8d8d8d (26.4% of the light) | #bcbcbc (50.5% of the light) | **47** | 1.9× | | black on a light page | 50% | #7b7b7b (19.6% of the light) | #b4b4b4 (45.7% of the light) | **57** | 2.3× | | two near neighbours | 50% | #b0b0b0 (43.4% of the light) | #b1b1b1 (43.9% of the light) | **1** | 1× | | brown and violet | 50% | #842080 (7.48% of the light) | #842cbc (10.4% of the light) | **60** | 1.4× | **Both columns are real.** CSS `opacity`, `rgba()` backgrounds and plain gradients interpolate the stored numbers; SVG `filter` primitives default to `color-interpolation-filters: linearRGB`, and colour-managed compositors work in light. On black and white the two are furthest apart at 71% opacity, where they differ by 73 levels. **And the error only ever runs one way.** The sRGB encode curve is concave, so mixing encoded values always lands at or below the encoding of the mixed light — on every channel, and therefore in luminance too. There is no pair of colours and no opacity for which the number-space blend is the brighter one. ## What it does to a contrast claim | The layer | Text | If numbers mixed | If light mixed | Same verdict? | | --- | --- | --- | --- | --- | | white glass at 18% on a dark photo | #ffffff | 9.86:1 (AAA) | 4.4:1 (AA large only) | **no** | | white glass at 50% on a dark photo | #ffffff | 3.34:1 (AA large only) | 1.89:1 (fails) | **no** | | a black scrim at 40% on a light page | #ffffff | 3.07:1 (AA large only) | 1.76:1 (fails) | **no** | | black and white at half | #000000 | 5.28:1 (AA) | 11:1 (AAA) | **no** | **WCAG contrast is defined on relative luminance — that is, on the light.** But the background it is measured against was produced by one of two interpolations, and 4 of these 4 land on opposite sides of a threshold depending on which. A ratio quoted without naming the pipeline is not a measurement. ## What it is A single-page app at https://gamma-desk.skillsafe.ai/. The engine is free and runs entirely in the browser with no account; writing the report costs credits. 5 lanes over one sheet, each handing its result to the next. ## The free engine `gamma.js` makes no network calls at all. Given a sheet it computes every layer in both pipelines, measures how far apart they land in 8-bit levels and in degrees of hue, computes the WCAG contrast ratio and verdict against the stated text colour for each result, and reports where the two verdicts disagree. It emits 31 findings. All of that is sent with every paid run as `prescan`, so the model is never asked to do arithmetic it can be given. Hue is only compared when both results carry at least 0.15 chroma. Below that a result is a neutral with a faint cast, and two faint casts can sit 180 degrees apart in hue while being the same colour to anybody looking at them — which is arithmetic rather than a finding. ## Sheet grammar ```text SCENE name | the glass card text | #ffffff pipeline | sRGB BLENDS b1 | the panel over the photo | #ffffff | 18% | #1a1a1a | white glass on a dark hero b2 | the drop shadow | #000000 | 40% | #f5f5f5 | on the light section below ``` A BLENDS row is `id | what it is | top colour | opacity | backdrop | why`. Colours are `#ffffff`, `#fff` or `rgb(255, 255, 255)`. Opacity is `18%` or `0.18` — a bare number above 1 is read as a percentage, because nobody writes an opacity of eighteen. In SCENE, **`text` is the colour sitting on top of every result**, and every contrast ratio on the page is measured against it; it defaults to `#ffffff` and the assumption is reported. **`pipeline` is optional and usually absent**, which is the point: `sRGB` says the design is authored the way CSS composites, `linear` says it is authored in light, and leaving it out is what almost every real design does — so both are computed and the difference is the finding. ## Lanes | task | Lane | Fields | Sections | | --- | --- | --- | --- | | `plan` | Decide the layers before anything is composited | `brief, known` | Summary, The Sheet, The Layers, Reasoning, Next Step | | `check` | What this scene actually produces | `sheet, worry` | Summary, Verdict, Findings, Corrected Sheet, Next Step | | `both` | Every layer, computed both ways | `sheet` | Summary, Both Ways, Where They Part, What Is At Stake, Next Step | | `contrast` | What the text can honestly claim | `sheet` | Summary, Every Verdict, Which Ones Flip, What Can Be Claimed, Next Step | | `deliver` | Decide what changes: a colour, the opacity, or the pipeline | `sheet, fixed` | Summary, A Colour Fixes, Only Pinning It Down Fixes, Nothing Fixes, Next Step | Every run body must carry `task`. The body IS the input object — do not wrap it in an `{"input": ...}` envelope, which returns 200 while hiding `task` from the model. ## Thresholds | Number | Value | What it decides | | --- | --- | --- | | a visible difference | 4 levels | where two results stop being the same colour on screen | | far apart | 24 levels or more | where anybody can see it side by side and it is no longer a subtlety | | a hue shift | 3° or more | where the two results are different colours rather than different brightnesses | | enough chroma to have a hue | 0.15 | below this a result is a neutral and its hue is arithmetic rather than colour | | the piecewise segment | below level 10 | where sRGB is a straight line and a gamma-2.2 approximation stops working | | AA on normal text | 4.5:1 | the threshold most of these verdicts turn on | | AA on large text | 3:1 | the lower bar | | AAA on normal text | 7:1 | the higher one | ## Severity Severity is fixed by the code. **Errors are reserved for the two cases where the specification has no single answer** — two pipelines producing visibly different pixels, and a WCAG verdict that depends on which one ran. A hue shift, a thin layer and a near-threshold ratio are warnings: real costs somebody has to decide about. | Code | Severity | Scope | What it means | | --- | --- | --- | --- | | `TEXT-ASSUMED` | warn | scene | The text colour was assumed | | `NO-BLENDS` | warn | scene | No blends given | | `ONE-BLEND` | note | scene | Only one blend | | `PIPELINES-DISAGREE` | error | blend | The two pipelines produce visibly different pixels | | `PIPELINES-DIFFER` | warn | blend | The two pipelines differ, slightly | | `PIPELINES-AGREE` | note | blend | Both pipelines land in the same place | | `SRGB-IS-DARKER` | warn | blend | Interpolating the stored numbers gives a darker result | | `SRGB-IS-NEVER-LIGHTER` | note | scene | Interpolating the numbers can never give a lighter result | | `MIDPOINT` | note | blend | This is the canonical case | | `OPACITY-EDGE` | note | blend | At this opacity both pipelines agree exactly | | `HUE-MOVES` | warn | blend | The two results are different colours, not just different brightnesses | | `GREY-THROUGHOUT` | note | blend | Both colours are neutral | | `CONTRAST-VERDICTS-DIFFER` | error | blend | The accessibility verdict depends on which pipeline blended the background | | `CONTRAST-BOTH-PASS` | note | blend | Both backgrounds clear the same threshold | | `CONTRAST-BOTH-FAIL` | warn | blend | Neither background clears AA | | `CONTRAST-NEAR-THRESHOLD` | warn | blend | One of the ratios sits right on a threshold | | `LOW-OPACITY` | note | blend | A thin layer, where the two pipelines diverge most | | `HIGH-OPACITY` | note | blend | A thick layer, where the backdrop barely matters | | `SAME-COLOURS` | note | blend | The two colours are the same | | `NEAR-BLACK` | note | blend | One of the colours is in the piecewise part of the curve | | `WORST-DISAGREEMENT` | note | scene | Where the two pipelines are furthest apart | | `ALL-AGREE` | note | scene | Every blend lands in the same place both ways | | `MOST-DISAGREE` | warn | scene | Most of this scene depends on which tool composites it | | `CSS-BLENDS-IN-SRGB` | note | scene | What CSS actually does | | `SVG-BLENDS-IN-LINEAR` | note | scene | What SVG filters actually do | | `RESIZING-IS-BLENDING` | note | scene | Every resize is this same arithmetic | | `HALF-IS-NOT-HALF` | note | scene | The number in the middle is not the light in the middle | | `STACKED-OVER-THE-SAME-BACKDROP` | note | scene | Several layers over the same backdrop | | `SATURATED-PAIR` | note | blend | Two saturated colours of different hues | | `TEXT-ON-EVERY-BLEND` | note | scene | The text colour is measured against every result | | `PIPELINE-NOT-STATED` | warn | scene | Nothing here records which pipeline is intended | ## Which question to ask **One thing decides more than any other, and it is not a colour: it is whether the layer is translucent at all.** Every finding on this page comes from interpolating between two colours. An opaque layer has nothing to interpolate, so both pipelines return it exactly and the design says what it means. So the useful order is: 1. **Does this layer need to be translucent?** Frosted glass over a photograph does. A card on a flat background does not — it can be the resulting colour, stated once. 2. **If it does, which pipeline is it authored in?** Write it down somewhere a tool reads, and stop quoting contrast ratios that do not name it. 3. **If it cannot be pinned down, design for the worse of the two.** The number-space result is always the darker one, so for light text that is the safe end and for dark text it is not. Most of what looks like a colour-management problem is a design that asked a medium to carry a decision the medium has no field for. ## API Full documentation at https://gamma-desk.skillsafe.ai/api.html. The engine is client-side only and has no endpoint, but everything it computes is above, so you can reproduce every figure in your own pipeline without this app. ## What this page cannot do - **It has not rendered anything.** These are the two arithmetics, not a screenshot of either, and a real compositor adds dithering, rounding and sometimes a wider working space. - **It models a simple source-over blend.** Backdrop blur, `mix-blend-mode`, layer stacks and filters compound this and are not on the sheet. - **Each row is measured against the backdrop as given**, not against what the row above produced. A real stack compounds the difference at every level. - **sRGB only.** Display-P3, Rec.2020 and the wide-gamut CSS colour spaces have their own transfer functions and none of them is this one. - **WCAG 2 contrast is itself a rough model.** It is what auditors use, and APCA disagrees with it about light text on dark backgrounds in particular. - **It says nothing about how a colour looks.** Perceptual uniformity is a different question from linear light, and Oklab answers it and this does not. ## Source Lanes derived from the `liquid-glass-design` skill in https://github.com/affaan-m/everything-claude-code, which builds translucent, layered interface surfaces. Every one of them is a colour, an opacity and a backdrop — three values that do not determine a pixel. Not affiliated with or endorsed by the authors of that repository.