# Palette — colour taken from the image

> Every asset carries the colours of its own photo. What each helper is for, shown next to the photograph that produced it, with the contrast ratios measured.

Every processed image ships a `palette` in its DTO — a handful of hexes lifted
from the photo itself. This page is not a table of hexes. It shows **what each
helper is for**, next to the photograph whose colours it used, and it publishes
the **measured** contrast ratio anywhere text sits on a palette colour.

Nothing here is invented. Five photographs, uploaded to the public `demo`
tenant, each chosen for a **different chromatic character** — a palette page
where every photo is the same temperature teaches nothing:

| photo | character | asset |
|---|---|---|
| **autumn** | warm / golden | `731fa52c-2407-45dc-b3c2-1511a616a057` |
| **glacier** | cool / blue | `b86b250a-00f3-4576-9504-6d8a8b100af3` |
| **night** | very dark | `20c0a8a1-9da4-45bf-b043-17fedc006300` |
| **snow** | very light | `5ce5f6df-74b3-4945-9d38-0b1cb70295d0` |
| **spices** | saturated | `e7b53096-59a8-4909-8093-7e410b3c28f1` |

Their licences and photographers are at [the bottom of this page](#the-photographs).
Every palette below came back from the API; re-fetch any of them and check:

```bash
# your own runtime key and tenant code — see /start/credentials/
export NITIDA_KEY="…"
export NITIDA_TENANT_CODE="…"

curl -s -H "Authorization: Bearer $NITIDA_KEY" \
     -H "X-Tenant-Code: $NITIDA_TENANT_CODE" \
  "https://api.nitida.gofuture.space/assets/<your-asset-id>"
```

The ids above belong to the documentation tenant, so they will not resolve
under your key — upload one of your own photographs and read the `palette` off
the DTO it returns. The shape is identical.

## The wire shape

The palette is deliberately tiny — one hex per swatch, and only the swatches
the source actually had. The long names map to 1–2 letter keys, because before
that the palette was 62 % of the asset DTO.

| key | role | what it usually is |
|---|---|---|
| `d` | dominant | the colour the eye reads as "this photo's colour" — always present |
| `v` | vibrant | the most saturated one — accents, never large surfaces |
| `m` | muted | desaturated mid-tone — the safest large surface |
| `lv` / `lm` | lightVibrant / lightMuted | the light end — page backgrounds |
| `dv` / `dm` | darkVibrant / darkMuted | the dark end — text, scrims, borders |

`population`, the RGB array and the text colour are **not** shipped: they are
derivable in the client, and `getTextColorForBackground` computes the last one
on demand.

## The five photographs

<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "1rem", margin: "1.5rem 0" }}>
  <figure style={{ margin: 0 }}>
    <img src="https://8ok.uk/d/v/818105b8e773f53c-s.webp" alt="Backlit yellow and orange maple leaves filling the frame, dark branches crossing them." width="640" height="427" style={{ width: "100%", height: "auto", borderRadius: "8px", display: "block" }} />
    <figcaption style={{ fontSize: "15px", lineHeight: 1.5, marginTop: ".5rem" }}><strong>autumn</strong> — warm / golden. Six swatches, and <strong>dominant and darkVibrant are the same hex</strong>, <code>#683600</code>.</figcaption>
  </figure>
  <figure style={{ margin: 0 }}>
    <img src="https://8ok.uk/d/v/99eb1affb492ff77-s.webp" alt="Close-up of a blue iceberg with sculpted texture, floating in calm dark water." width="640" height="427" style={{ width: "100%", height: "auto", borderRadius: "8px", display: "block" }} />
    <figcaption style={{ fontSize: "15px", lineHeight: 1.5, marginTop: ".5rem" }}><strong>glacier</strong> — cool / blue. Dominant <code>#010102</code>, which is the dark rock, not the ice. No <code>lightVibrant</code> at all.</figcaption>
  </figure>
  <figure style={{ margin: 0 }}>
    <img src="https://8ok.uk/d/v/6944dff2927c0be5-s.webp" alt="Silhouetted conifers behind low fog at twilight, a single warm street lamp at the left." width="640" height="427" style={{ width: "100%", height: "auto", borderRadius: "8px", display: "block" }} />
    <figcaption style={{ fontSize: "15px", lineHeight: 1.5, marginTop: ".5rem" }}><strong>night</strong> — very dark. Only <strong>four</strong> swatches: no vibrant, no lightVibrant, no lightMuted. This is the photo that breaks a helper, below.</figcaption>
  </figure>
  <figure style={{ margin: 0 }}>
    <img src="https://8ok.uk/d/v/52064d8246757b0c-s.webp" alt="A stand of bare larches on a snow field under a white winter sky." width="640" height="427" style={{ width: "100%", height: "auto", borderRadius: "8px", display: "block" }} />
    <figcaption style={{ fontSize: "15px", lineHeight: 1.5, marginTop: ".5rem" }}><strong>snow</strong> — very light. Also four swatches, at the opposite end: dominant <code>#FAFAF9</code> is all but white.</figcaption>
  </figure>
  <figure style={{ margin: 0 }}>
    <img src="https://8ok.uk/d/v/daf433475992139e-s.webp" alt="Cones of brightly coloured ground spices on a market stall." width="640" height="427" style={{ width: "100%", height: "auto", borderRadius: "8px", display: "block" }} />
    <figcaption style={{ fontSize: "15px", lineHeight: 1.5, marginTop: ".5rem" }}><strong>spices</strong> — saturated. The only one of the five with all seven swatches.</figcaption>
  </figure>
</div>

Their palettes, verbatim from the API:

```json
// autumn  — no "lm"
{ "d":"#683600", "v":"#D8A705", "m":"#C2AA48", "dv":"#683600", "lv":"#FCFA85", "dm":"#837C3D" }
// glacier — no "lv"
{ "d":"#010102", "v":"#379AB4", "m":"#97B5BA", "dv":"#044669", "dm":"#376877", "lm":"#A7C2C6" }
// night   — four swatches only
{ "d":"#151818", "m":"#4C8D91", "dv":"#015B68", "dm":"#151818" }
// snow    — four swatches only
{ "d":"#FAFAF9", "m":"#A7AAB5", "dm":"#57565A", "lm":"#EAE9EB" }
// spices  — all seven
{ "d":"#A93400", "v":"#D97501", "m":"#986737", "dv":"#A93400", "lv":"#F9B777", "dm":"#5B4328", "lm":"#E7C9C7" }
```

**Four of the five are missing at least one swatch.** That is the normal case,
not the edge case — write your CSS accordingly.

## `pickAmbientBackground` — stop the photo floating on white

A photo on `#FFFFFF` looks like a photo pasted onto a page. The same photo on a
surface borrowed from itself looks like it belongs there. That is the whole
job.

<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: "1rem", margin: "1.5rem 0" }}>
  <div>
    <p style={{ fontSize: "14px", letterSpacing: ".06em", textTransform: "uppercase", margin: "0 0 .4rem" }}>Plain white</p>
    <div style={{ background: "#FFFFFF", padding: "24px", borderRadius: "12px", border: "1px solid #d5dedc" }}>
      <img src="https://8ok.uk/d/v/52064d8246757b0c-s.webp" alt="Snow photo on a white surface" width="640" height="427" style={{ width: "100%", height: "auto", borderRadius: "8px", display: "block" }} />
    </div>
  </div>
  <div>
    <p style={{ fontSize: "14px", letterSpacing: ".06em", textTransform: "uppercase", margin: "0 0 .4rem" }}>pickAmbientBackground → <code>#EAE9EB</code></p>
    <div style={{ background: "#EAE9EB", padding: "24px", borderRadius: "12px", border: "1px solid #d5dedc" }}>
      <img src="https://8ok.uk/d/v/52064d8246757b0c-s.webp" alt="Snow photo on its own ambient colour" width="640" height="427" style={{ width: "100%", height: "auto", borderRadius: "8px", display: "block" }} />
    </div>
  </div>
</div>

```ts
import { pickAmbientBackground } from "@nitida/asset-client";

const ambient = pickAmbientBackground(asset.palette);
// snow → { hex: "#EAE9EB", textColor: "#000000" }
<figure style={{ background: ambient?.hex ?? "transparent" }}>…</figure>
```

It prefers the **light** end — `lightMuted → muted → lightVibrant → dominant` —
because a vibrant surface fights the photo instead of holding it. Measured on
our five:

| photo | ambient hex | role it came from | text it suggests | ratio |
|---|---|---|---|---|
| autumn | `#C2AA48` | muted (it has no `lm`) | `#FFFFFF` | **2.30:1** |
| glacier | `#A7C2C6` | lightMuted | `#000000` | 11.18:1 |
| night | `#4C8D91` | muted (it has no `lm`) | `#FFFFFF` | **3.80:1** |
| snow | `#EAE9EB` | lightMuted | `#000000` | 17.35:1 |
| spices | `#E7C9C7` | lightMuted | `#000000` | 13.58:1 |

The two rows in bold are not typos and they are not hidden: the two photos with
**no `lightMuted`** fall through to `muted`, a mid-tone, and the suggested white
text lands at 2.30:1 and 3.80:1. Why that happens, and what to do about it, is
[the limit](#the-limit-the-helper-flips-at-the-wrong-luminance) below.

## `getAmbientGradient` — the same idea with a direction

<div style={{ margin: "1.5rem 0", background: "linear-gradient(135deg, #E7C9C7, #986737)", padding: "28px", borderRadius: "12px", display: "flex", gap: "20px", alignItems: "center", flexWrap: "wrap" }}>
  <img src="https://8ok.uk/d/v/daf433475992139e-s.webp" alt="Spice photo over its ambient gradient" width="640" height="427" style={{ width: "200px", height: "auto", borderRadius: "8px", display: "block" }} />
  <p style={{ fontSize: "16px", lineHeight: 1.6, color: "#000000", margin: 0, maxWidth: "34ch" }}>Text sitting on the gradient of <strong>spices</strong>. Black on the light end measures <strong>13.58:1</strong>, and on the dark end <strong>4.33:1</strong> — the worst point on that surface is below the 4.5:1 body-text floor, so this text is only safe because it is large.</p>
</div>

```ts
getAmbientGradient(asset.palette);
// "linear-gradient(135deg, #E7C9C7, #986737)"   ← spices
getAmbientGradient(asset.palette, { angle: "180deg", from: "lv", to: "dv" });
```

The default runs `lightMuted → muted`. **Measure both ends, not one:** a
gradient is two backgrounds, and the text has to survive the worse one.

| photo | gradient | text | light end | dark end | worst end |
|---|---|---|---|---|---|
| autumn | `#C2AA48` → `#C2AA48` | `#FFFFFF` | 2.30:1 | 2.30:1 | **fails** |
| glacier | `#A7C2C6` → `#97B5BA` | `#000000` | 11.18:1 | 9.63:1 | passes AA |
| night | `#4C8D91` → `#4C8D91` | `#FFFFFF` | 3.80:1 | 3.80:1 | large text only |
| snow | `#EAE9EB` → `#A7AAB5` | `#000000` | 17.35:1 | 9.06:1 | passes AA |
| spices | `#E7C9C7` → `#986737` | `#000000` | 13.58:1 | 4.33:1 | large text only |

:::caution[A "gradient" can come back with two identical stops]
Look at **autumn** and **night**: `#C2AA48 → #C2AA48` and `#4C8D91 → #4C8D91`.
When a palette has no `lightMuted`, both ends of the default gradient resolve to
`muted` — the CSS is valid, it renders a flat colour, and nothing anywhere
reports a problem. If your design depends on the gradient being visible, compare
the two stops and fall back to a flat surface deliberately rather than by
accident.
:::

## `getTextColorForBackground` — and where it stops working

The helper answers one question: black or white on this hex? It computes WCAG
relative luminance and flips at **0.5**.

| photo | dominant | helper picks | ratio | the other option | verdict |
|---|---|---|---|---|---|
| autumn | `#683600` | `#FFFFFF` | 9.92:1 | `#000000` 2.12:1 | correct |
| glacier | `#010102` | `#FFFFFF` | 20.86:1 | `#000000` 1.01:1 | correct |
| night | `#151818` | `#FFFFFF` | 17.86:1 | `#000000` 1.18:1 | correct |
| snow | `#FAFAF9` | `#000000` | 20.11:1 | `#FFFFFF` 1.04:1 | correct |
| spices | `#A93400` | `#FFFFFF` | 6.61:1 | `#000000` 3.18:1 | correct |

On the **dominant** swatch the helper is right five times out of five — every
one of these photos has a dominant at one end of the range. It is the mid-tones
that break it. The clearest case is **glacier**'s `muted`, the pale ice blue:

<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "1rem", margin: "1.5rem 0" }}>
  <div>
    <p style={{ fontSize: "14px", letterSpacing: ".06em", textTransform: "uppercase", margin: "0 0 .4rem" }}>What the helper returns</p>
    <div style={{ background: "#97B5BA", color: "#FFFFFF", padding: "20px", borderRadius: "12px" }}>
      <p style={{ fontSize: "16px", lineHeight: 1.55, margin: 0 }}>Glaciar Perito Moreno · 4 h de navegación. White on <code style={{ color: "inherit", background: "none" }}>#97B5BA</code> measures 2.18:1.</p>
    </div>
  </div>
  <div>
    <p style={{ fontSize: "14px", letterSpacing: ".06em", textTransform: "uppercase", margin: "0 0 .4rem" }}>What measures better</p>
    <div style={{ background: "#97B5BA", color: "#000000", padding: "20px", borderRadius: "12px" }}>
      <p style={{ fontSize: "16px", lineHeight: 1.55, margin: 0 }}>Glaciar Perito Moreno · 4 h de navegación. Black on <code style={{ color: "inherit", background: "none" }}>#97B5BA</code> measures 9.63:1.</p>
    </div>
  </div>
</div>

### The limit: the helper flips at the wrong luminance

Black and white tie at relative luminance **L ≈ 0.1791**
(`√(1.05 × 0.05) − 0.05`), not at 0.5. Between 0.1791 and 0.5 the helper
answers white where black scores higher — and every mid-light saturated swatch
lands in that band. Across the five palettes on this page, **8 swatches** fall
there:

| photo | swatch | role | L | helper says | better | helper's ratio | best ratio |
|---|---|---|---|---|---|---|---|
| autumn | `#D8A705` | vibrant | 0.422 | `#FFFFFF` | `#000000` | 2.22:1 | 9.45:1 |
| autumn | `#C2AA48` | muted | 0.407 | `#FFFFFF` | `#000000` | 2.30:1 | 9.14:1 |
| autumn | `#837C3D` | darkMuted | 0.196 | `#FFFFFF` | `#000000` | 4.27:1 | 4.92:1 |
| glacier | `#379AB4` | vibrant | 0.272 | `#FFFFFF` | `#000000` | 3.26:1 | 6.44:1 |
| glacier | `#97B5BA` | muted | 0.432 | `#FFFFFF` | `#000000` | 2.18:1 | 9.63:1 |
| night | `#4C8D91` | muted | 0.226 | `#FFFFFF` | `#000000` | 3.80:1 | 5.53:1 |
| snow | `#A7AAB5` | muted | 0.403 | `#FFFFFF` | `#000000` | 2.32:1 | 9.06:1 |
| spices | `#D97501` | vibrant | 0.275 | `#FFFFFF` | `#000000` | 3.23:1 | 6.50:1 |

**Every single one of the five photographs has at least one swatch in that
band**, and in all eight cases the answer the helper gives is the worse of the
two.

:::caution[Do not put body text on a palette colour without measuring it]
`getTextColorForBackground` is safe at the ends of the range — near-black and
near-white swatches — and unreliable in the middle. Either pick by ratio, or
put your text on `darkMuted`/`darkVibrant` (dependably dark) and leave the
mid-tones for surfaces that carry no text.
:::

Nine lines pick by ratio instead of by threshold:

```ts
const lum = (hex: string) => {
  const n = Number.parseInt(hex.slice(1), 16);
  const ch = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => {
    const s = c / 255;
    return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
};
/** Whichever of black/white actually wins on this hex. */
export const bestTextColor = (hex: string) =>
  (lum(hex) + 0.05) / 0.05 > 1.05 / (lum(hex) + 0.05) ? "#000000" : "#FFFFFF";
```

## A scrim: measure the colour **with its alpha resolved**

The most common way a caption meets a photo is a translucent bar. And this is
where a measured ratio quietly becomes a fictional one: `rgba(#5B4328, 0.72)`
over a bright photo **is not** `#5B4328`, so measuring the nominal hex
overstates the contrast — by nearly 2× in the rows below.

<div style={{ position: "relative", margin: "1.5rem 0", borderRadius: "12px", overflow: "hidden", maxWidth: "520px" }}>
  <img src="https://8ok.uk/d/v/daf433475992139e-s.webp" alt="Spice photo with a caption bar over it" width="640" height="427" style={{ width: "100%", height: "auto", display: "block" }} />
  <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, background: "rgba(91, 67, 40, 0.72)", padding: "14px 16px" }}>
    <p style={{ fontSize: "16px", lineHeight: 1.5, color: "#FFFFFF", margin: 0 }}>Mercado de especias — white on the <em>resolved</em> bar measures 5.41:1 against this photo's lightest swatch, not the 9.22:1 the nominal hex suggests.</p>
  </div>
</div>

| photo | nominal scrim (`dm`) | resolved over its lightest swatch | white on **nominal** | white on **resolved** |
|---|---|---|---|---|
| autumn | `#837C3D` | `#A59F51` over `#FCFA85` | 4.27:1 | **2.74:1** |
| glacier | `#376877` | `#56818D` over `#A7C2C6` | 6.16:1 | 4.27:1 |
| night | `#151818` | `#151818` over `#151818` | 17.86:1 | 17.86:1 |
| snow | `#57565A` | `#807F83` over `#EAE9EB` | 7.28:1 | 3.98:1 |
| spices | `#5B4328` | `#87633E` over `#F9B777` | 9.22:1 | 5.41:1 |

**autumn** is the case worth staring at: the nominal `4.27:1` already looks
tight, and the resolved value is **2.74:1** — a caption nobody can read, over a
photograph everybody will like. Only **spices** and **night** clear AA once the
alpha is resolved; **glacier** and **snow** land in large-text-only territory.

`night` resolves to itself because its `darkMuted` and its `dominant` are the
same hex and it has no light swatch at all — there is nothing lighter in that
photo for the bar to sit on.

The backdrop used here is each photo's own lightest swatch, i.e. the worst case
that image can offer.

## `getPaletteCssVars` — hand the whole palette to CSS

One call on a wrapper, and every descendant can read the colours without any
component knowing which asset it is rendering.

```tsx
import { getPaletteCssVars } from "@nitida/asset-client";

<article style={getPaletteCssVars(asset.palette)} className="listing">
  <img src={url} alt="" />
  <h3>Glaciar Perito Moreno</h3>
</article>
```

```css
.listing        { background: var(--asset-bg); color: var(--asset-fg); }
.listing h3     { color: var(--asset-dark-vibrant, var(--asset-fg)); }
.listing .price { border-inline-start: 3px solid var(--asset-vibrant, currentColor); }
```

For **glacier** the call returns exactly this — eight variables, no invention,
and note that `--asset-light-vibrant` is simply **not there**:

```css
--asset-bg: #A7C2C6;
--asset-fg: #000000;
--asset-dominant: #010102;
--asset-vibrant: #379AB4;
--asset-muted: #97B5BA;
--asset-dark-vibrant: #044669;
--asset-light-muted: #A7C2C6;
--asset-dark-muted: #376877;
```

And rendered, using those variables and nothing else:

<article style={{ background: "#A7C2C6", color: "#000000", borderRadius: "12px", padding: "20px", display: "flex", gap: "16px", alignItems: "center", flexWrap: "wrap", margin: "1.5rem 0" }}>
  <img src="https://8ok.uk/d/v/99eb1affb492ff77-s.webp" alt="Iceberg photo inside a card styled from its own palette" width="640" height="427" style={{ width: "180px", height: "auto", borderRadius: "8px", display: "block" }} />
  <div style={{ minWidth: "16rem", flex: 1 }}>
    <h3 style={{ color: "#044669", margin: "0 0 .35rem", fontSize: "20px" }}>Glaciar Perito Moreno</h3>
    <p style={{ fontSize: "16px", lineHeight: 1.6, margin: "0 0 .6rem" }}>Body text at 16px on <code>--asset-bg</code>, using <code>--asset-fg</code>: black on <code>#A7C2C6</code> measures 11.18:1. The heading uses <code>--asset-dark-vibrant</code> — <code>#044669</code> on the same background measures 5.36:1, which clears AA for body text.</p>
    <p style={{ fontSize: "16px", margin: 0, paddingInlineStart: "10px", borderInlineStart: "3px solid #379AB4" }}>USD 240 / persona</p>
  </div>
</article>

The same card, measured for all five — because a heading colour that works on
one photo is not a heading colour that works:

| photo | ambient | body text (picked by ratio) | body ratio | heading (`dv`) | heading ratio |
|---|---|---|---|---|---|
| autumn | `#C2AA48` | `#000000` | 9.14:1 | `#683600` | 4.32:1 — large text only |
| glacier | `#A7C2C6` | `#000000` | 11.18:1 | `#044669` | 5.36:1 — passes AA |
| night | `#4C8D91` | `#000000` | 5.53:1 | `#015B68` | **2.05:1 — fails** |
| snow | `#EAE9EB` | `#000000` | 17.35:1 | `#57565A` | 6.02:1 — passes AA |
| spices | `#E7C9C7` | `#000000` | 13.58:1 | `#A93400` | 4.27:1 — large text only |

**`darkVibrant` as a heading colour is not dependable.** On **night** the
ambient surface is itself a mid-tone teal and the "dark" vibrant is another
teal: 2.05:1, invisible. Two more land under 4.5:1. If the heading must be
legible, pick it by ratio like the body text.

Two gotchas worth knowing before you wire this up:

- **`--asset-bg` is the ambient pick, not the dominant.** They are different
  colours in **all five** of these photos. If you want the dominant, use
  `--asset-dominant`.
- **The optional vars are absent, not empty**, when the source had no such
  swatch — glacier ships no `--asset-light-vibrant`, night ships neither that
  nor `--asset-vibrant` nor `--asset-light-muted`. Always give `var()` a
  fallback: `var(--asset-vibrant, currentColor)`.

## `iteratePaletteSwatches` — the strip for an admin UI

Display order, dominant first, missing swatches skipped. **glacier**, with what
each swatch can and cannot carry — every label below is drawn in the colour that
*measures* best, and the ratio printed is the one actually rendered:

<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: ".75rem", margin: "1.5rem 0" }}>
  <div style={{ background: "#010102", color: "#FFFFFF", padding: "14px", borderRadius: "10px" }}>
    <p style={{ fontSize: "14px", letterSpacing: ".04em", textTransform: "uppercase", margin: "0 0 .3rem" }}>dominant</p>
    <p style={{ fontSize: "15px", margin: 0 }}>#010102 · 20.86:1</p>
  </div>
  <div style={{ background: "#379AB4", color: "#000000", padding: "14px", borderRadius: "10px" }}>
    <p style={{ fontSize: "14px", letterSpacing: ".04em", textTransform: "uppercase", margin: "0 0 .3rem" }}>vibrant</p>
    <p style={{ fontSize: "15px", margin: 0 }}>#379AB4 · 6.44:1 with black</p>
  </div>
  <div style={{ background: "#044669", color: "#FFFFFF", padding: "14px", borderRadius: "10px" }}>
    <p style={{ fontSize: "14px", letterSpacing: ".04em", textTransform: "uppercase", margin: "0 0 .3rem" }}>darkVibrant</p>
    <p style={{ fontSize: "15px", margin: 0 }}>#044669 · 10.07:1</p>
  </div>
  <div style={{ background: "#97B5BA", color: "#000000", padding: "14px", borderRadius: "10px" }}>
    <p style={{ fontSize: "14px", letterSpacing: ".04em", textTransform: "uppercase", margin: "0 0 .3rem" }}>muted</p>
    <p style={{ fontSize: "15px", margin: 0 }}>#97B5BA · 9.63:1 with black</p>
  </div>
  <div style={{ background: "#A7C2C6", color: "#000000", padding: "14px", borderRadius: "10px" }}>
    <p style={{ fontSize: "14px", letterSpacing: ".04em", textTransform: "uppercase", margin: "0 0 .3rem" }}>lightMuted</p>
    <p style={{ fontSize: "15px", margin: 0 }}>#A7C2C6 · 11.18:1</p>
  </div>
  <div style={{ background: "#376877", color: "#FFFFFF", padding: "14px", borderRadius: "10px" }}>
    <p style={{ fontSize: "14px", letterSpacing: ".04em", textTransform: "uppercase", margin: "0 0 .3rem" }}>darkMuted</p>
    <p style={{ fontSize: "15px", margin: 0 }}>#376877 · 6.16:1</p>
  </div>
</div>

The **vibrant** and **muted** cards carry black text even though the helper
would have returned white for both — the same 0.5-threshold limit, on a real
palette. `getTextColorForBackground` would have put white on `#97B5BA` at
2.18:1.

Note also that a strip can contain the same hex twice: in **night**,
`dominant` and `darkMuted` are both `#151818`. Deduplicate before you draw a
strip, or a designer will report a rendering bug that is really just a dark
photograph.

## `getPaletteBlurBackground` — a placeholder that costs zero extra bytes

The palette is already in the DTO, so this abstract "colour cloud" ships for
free — no LQIP to fetch, no base64 to inline. Four radial gradients anchored at
fixed corners over the dominant fill.

<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: "1rem", margin: "1.5rem 0" }}>
  <div>
    <p style={{ fontSize: "14px", letterSpacing: ".06em", textTransform: "uppercase", margin: "0 0 .4rem" }}>Placeholder · spices</p>
    <div style={{ aspectRatio: "3 / 2", borderRadius: "10px", background: "radial-gradient(circle at 20% 20%, #F9B777 0%, transparent 55%), radial-gradient(circle at 80% 25%, #D97501 0%, transparent 55%), radial-gradient(circle at 25% 80%, #E7C9C7 0%, transparent 55%), radial-gradient(circle at 80% 80%, #A93400 0%, transparent 55%), #A93400" }} />
  </div>
  <div>
    <p style={{ fontSize: "14px", letterSpacing: ".06em", textTransform: "uppercase", margin: "0 0 .4rem" }}>The photo it stands in for</p>
    <img src="https://8ok.uk/d/v/daf433475992139e-s.webp" alt="Spice market photo" width="640" height="427" style={{ width: "100%", aspectRatio: "3 / 2", objectFit: "cover", borderRadius: "10px", display: "block" }} />
  </div>
  <div>
    <p style={{ fontSize: "14px", letterSpacing: ".06em", textTransform: "uppercase", margin: "0 0 .4rem" }}>Placeholder · night (four swatches)</p>
    <div style={{ aspectRatio: "3 / 2", borderRadius: "10px", background: "radial-gradient(circle at 80% 80%, #015B68 0%, transparent 55%), #151818" }} />
  </div>
</div>

The third block is **night**, whose palette has four entries: three of the four
corners are simply **not emitted**, and the base fill shows through almost
everywhere. Missing swatches degrade a corner at a time — they never produce
`undefined` inside a CSS string.

:::caution[Do not put text on this surface]
It is a composite of overlapping translucent gradients, so no single hex is
"the background colour" — the colour under your text depends on where the text
sits. There is nothing honest to measure. Text goes on a scrim, as above.
:::

## Re-measuring

Every number on this page is printed by a script that imports the real helpers
from `@nitida/asset-client` — none of them were estimated. Nothing about it is
privileged: it uploads the five photographs with an ordinary **runtime** key,
reads the `palette` back off each DTO, and computes every contrast ratio from
those hexes with the exported helpers. You can do the same on your own tenant:

```bash
export NITIDA_KEY="…"          # your runtime key — see /start/credentials/
export NITIDA_TENANT_CODE="…"  # your tenant code
```

The palettes are never retyped into this page — they are read from the JSON the
upload step wrote — so changing a helper and re-running is enough to tell
whether this page is still true.

For what these same photographs weigh at every width and quality, and for the
one place where asking for a `quality` costs you image quality, see
[the transform benchmark](/guides/transform-benchmark/).

## The photographs

All five are from [Pexels](https://www.pexels.com/) under the
[Pexels License](https://www.pexels.com/license/), verified on 2026-08-17:
*"All photos and videos on Pexels are free to use"*, and *"attribution is not
required"*. We credit anyway — it costs a line.

| photo | photographer | source |
|---|---|---|
| autumn | [Ekaterina Swiss](https://www.pexels.com/@ekaterina-swiss-3802271) | [Pexels 11696183](https://www.pexels.com/photo/yellow-maple-leaves-on-a-tree-11696183/) · 6000×4000 |
| glacier | [Lars H Knudsen](https://www.pexels.com/@farlight) | [Pexels 13314282](https://www.pexels.com/photo/close-up-of-a-glacier-13314282/) · 9504×6336 |
| night | [Andrea Bova](https://www.pexels.com/@andrea-bova-1169228) | [Pexels 5871563](https://www.pexels.com/photo/silhouette-of-trees-at-night-5871563/) · 6000×4000 |
| snow | [Perati Wattanawikkarn](https://www.pexels.com/@perati-wattanawikkarn-2149005101) | [Pexels 30471757](https://www.pexels.com/photo/serene-winter-landscape-with-snow-covered-trees-30471757/) · 6000×4000 |
| spices | [Mike van Schoonderwalt](https://www.pexels.com/@mike-van-schoonderwalt-1884800) | [Pexels 5504603](https://www.pexels.com/photo/orange-and-red-spices-on-street-market-5504603/) · 6000×4000 |

Those fields live in a single manifest that the upload script reads. A
photograph that is not in it does not go on this site.