# Styling tokens

> The --andor-* CSS custom properties that restyle every AndOr widget, how they chain, and which value wins.

Source: https://backtrackhq.com/docs/developers/tokens/

Styling tokens are CSS custom properties named `--andor-*`. Every AndOr
widget reads them, so you can restyle all widgets with one global token,
or one widget with a per-widget token.

There are two layers, chained by `var()` fallback:

- **Global tokens** change every widget at once. Example:
  `--andor-color-accent`.
- **Per-widget tokens** change one widget. Example:
  `--andor-badge-background`.

A merchant who never opens a setting gets the shipped look.

## Where to put your CSS

All of these work equally well:

- The theme's own stylesheet. On Dawn, that is `assets/base.css`. On
  Horizon, use `assets/base.css` or a snippet you render in
  `layout/theme.liquid`.
- Theme settings, **Custom CSS**, in the theme editor.
- The **Custom CSS** box in the AndOr app embed. It is printed after the
  token block, so a `:root` rule there wins without specificity games.

```liquid
<style>
  :root { /* tokens */ }
</style>
<style>
  /* the Custom CSS field, verbatim */
</style>
```

## Global tokens

The app embed prints these into `:root`. With the embed off they are
absent, and the fallbacks in the stylesheet give the same look.

| Token | As emitted | Set from |
|---|---|---|
| `--andor-sale-color` | `#c0392b` | embed setting "Sale price color" |
| `--andor-original-color` | `#6d7175` | embed setting "Original price color" |
| `--andor-font-family` | `inherit` | not settable, override in CSS |
| `--andor-color-text` | `inherit` | not settable, override in CSS |
| `--andor-color-accent` | `var(--andor-sale-color, #c0392b)` | follows the sale color |
| `--andor-color-on-accent` | `#ffffff` | not settable, override in CSS |
| `--andor-color-muted` | `var(--andor-original-color, #6d7175)` | follows the original color |
| `--andor-color-border` | `rgba(0, 0, 0, 0.08)` | not settable, set in CSS |
| `--andor-radius` | `4px` | embed setting "Corner radius", range 0-24 |
| `--andor-space` | `0.4em` | not settable, override in CSS |

- `--andor-sale-color` follows the app brand color while the embed's
  **Sale price color** stays at its default. For example, a brand color
  of `#0b6b4f` with the sale color untouched gives
  `--andor-sale-color: #0b6b4f`. Not every widget uses the brand color.
  See
  [Brand color](https://backtrackhq.com/docs/promotions/brand-color/).
- `--andor-sale-color` and `--andor-original-color` are the older pair.
  The newer names default through them, so an existing override keeps
  working.
- When **Show reward icons on goal bars** is off, the embed also prints
  `--andor-goal-icons: none`.

### Opt-in tokens

The embed does not print these. Set them in CSS, or pick a
[style preset](https://backtrackhq.com/docs/developers/presets/). Unset, nothing changes.

| Token | Read by | Unset falls back to |
|---|---|---|
| `--andor-color-success` | goal fill once won | the goal accent |
| `--andor-color-background` | goal, claim, cards | `transparent` |
| `--andor-color-background-soft` | tiers current row | `rgba(0,0,0,.04)` |

### Skeleton tokens

Widgets printed by JavaScript show a quiet shimmer while they wait. It
stands still under `prefers-reduced-motion: reduce`.

| Property | Default | Controls |
|---|---|---|
| `--andor-skeleton-color` | *border*, 8% ink | the fill |
| `--andor-skeleton-highlight` | `transparent` | the moving band |
| `--andor-skeleton-duration` | `1.6s` | one pass of the band |
| `--andor-skeleton-radius` | *radius*, `4px` | corners, unframed parts |

```css
:root {
  --andor-skeleton-color: rgba(var(--color-foreground), 0.06);
  --andor-skeleton-highlight: rgba(var(--color-foreground), 0.02);
}
```

### Goal bar icon position

`--andor-goal-icon-spot` is read by a script when the bar is first
printed. The value `label` puts each step's icon beside its label. Any
other value, or none, keeps the icon inside the step's circle.

```css
:root {
  --andor-goal-icon-spot: label;
}
```

## Per-widget tokens

Per-widget tokens are named `--andor-<widget>-<property>`. Each one
falls back to a global token where one exists, then to a literal:

```css
background: var(
  --andor-badge-background,
  var(--andor-color-accent, #c0392b)
);
```

Some examples:

| Token | Falls back to | Literal | Widget |
|---|---|---|---|
| `--andor-price-gap` | `--andor-space` | `0.4em` | price |
| `--andor-badge-background` | `--andor-color-accent` | `#c0392b` | badge |
| `--andor-badge-color` | `--andor-color-on-accent` | `#ffffff` | badge |
| `--andor-badge-radius` | | `0.25em` | badge |
| `--andor-badge-font-size` | | `0.85em` | badge |
| `--andor-countdown-color` | `--andor-color-accent` | `#c0392b` | countdown |
| `--andor-offers-accent` | `--andor-color-accent` | `#c0392b` | offers |
| `--andor-price-space-block-start` | | `0` | price |
| `--andor-price-space-block-end` | | `0` | price |

Every widget's full list is on its styling page. For example, see
[Goal bar styling](https://backtrackhq.com/docs/widgets/goal-bar/styling/) and
[Savings badge styling](https://backtrackhq.com/docs/widgets/savings-badge/styling/).

> **Note:** `--andor-badge-radius` does not follow `--andor-radius`. The
> badge keeps its own `0.25em` default.

## Which value wins

Highest to lowest:

1. **An inline `style` on the widget's host.** The badge's background
   and text, the countdown's text and the offers accent are always
   inline. The accent of the volume tiers, goal bar, offer picker and
   claim rows is inline only when the merchant changed it from
   `#c0392b`.
2. **A block-scoped rule**, `[data-andor-widget-id="<block id>"]`,
   printed by the block when a setting differs from its default.
3. **A per-widget token** set anywhere in your CSS.
4. **A global token** set anywhere in your CSS.
5. **The literal fallback** in the AndOr stylesheet.

Levels 2 and 3 have the same specificity. The block's rule wins because
it comes later in the page. Raise your selector's specificity to beat
it.

Two consequences:

- A stylesheet rule on `--andor-badge-background` does not beat the
  inline color, even when the merchant never touched it. Override the
  property itself (`andor-badge { background: … }`) or use
  `!important`.
- An inline custom property can also be beaten from a child element,
  where your value wins by inheritance:

```css
andor-goal .andor-goal__bar {
  --andor-goal-accent: #0b6b4f;
}
```

## Examples

Retune the brand once, at `:root`. Blocks still on their default colors
follow it. Inline colors still win, as shown in
[Which value wins](#which-value-wins):

```css
:root {
  --andor-color-accent: #0b6b4f;
  --andor-color-on-accent: #ffffff;
  --andor-color-muted: #5c6670;
  --andor-color-border: rgba(11, 107, 79, 0.18);
  --andor-radius: 999px;
  --andor-space: 0.5em;
}
```

Then change one widget only:

```css
:root {
  /* Pill badge, larger, in the brand's secondary color. */
  --andor-badge-background: #f4f1ea;
  --andor-badge-color: #1c1b19;
  --andor-badge-radius: 999px;
  --andor-badge-padding: 0.25em 0.75em;
  --andor-badge-font-size: 0.9em;
}

/* Every widget has space-block-start and -end tokens. */
:root {
  --andor-price-space-block-start: 0.5rem;
  --andor-price-space-block-end: 0.75rem;
}

.andor-offers__item {
  padding-block: 0.6em;
}
```

## Keep in mind

- **Contrast is yours to check.** Target at least 4.5:1 for normal text.
  The shipped default is `#ffffff` on `#c0392b`.
- **Never force a hidden widget back on.** Hidden widgets carry both
  `hidden` and an inline `display: none`. A rule like
  `andor-badge { display: block }` would bring back an expired sale.
- **Do not restyle `.andor-vh`.** It is the visually hidden text for
  screen readers.
