# Phase 5 — Production conversion and branding

## Removing the SANDBOX badge

The **SANDBOX** banner in the app header is driven by the **environment type**, not by any
theme setting. No amount of theming will remove it. You must convert the environment to
**Production**.

### How

**PPAC → Environments → [your environment] → Convert to production** → Continue.

Takes about a minute. Verify on the environment hub: **Type: Production**, and check
**Recent operations** shows `Convert / Succeeded`.

> `pac admin list` will still report `Sandbox` after a successful conversion. It is wrong.
> Trust PPAC.

### What you give up

| | Sandbox | Production |
|---|---|---|
| SANDBOX badge | shown | **gone** |
| **Reset** (wipe & restart) | available | **lost** |
| **Copy into** this environment | available | **lost** |
| Backups | full | still backed up; restore targets a sandbox |

Reversible — PPAC offers **Convert to sandbox**. For a demo environment you have invested hours
in configuring, losing one-click Reset is usually a feature, not a cost.

### Client-side caching

The badge may persist in an open browser session after conversion. A normal reload usually
clears it. **Do not clear cookies to force it** — that signs you out of the tenant and you will
need to re-authenticate. (Learned the hard way.)

---

## Branding — the legacy theme entity does nothing

**Critical:** since **April 2026 (2026 Wave 1)** the modern Fluent 2 look is **mandatory** for
all model-driven apps, and it explicitly **does not honour classic theming**.

- <https://learn.microsoft.com/en-us/power-apps/user/modern-fluent-design>
- <https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/modern-theme-overrides>

> *"With the modern, refreshed look, Power Apps no longer honors classic theme customizations."*

Creating a `theme` record and calling `PublishTheme` returns **HTTP 204** and sets
`isdefaulttheme = true` — and changes **nothing** visually. The header stays Fluent 2 default
`#021838`. This is expected behaviour, not a misconfiguration.

---

## The supported modern theming path

Three pieces. **All three are required.**

### 1. Create an XML web resource

Entity is `webresource` (**not** `webresourceset` — that is the OData collection name).
`webresourcetype: 4` = Data (XML). Content must be base64.

```js
const xml =
  '<CustomTheme basePaletteColor="#0E4C5A" lockPrimary="true" logoTooltip="Caldova">' +
  '  <AppHeaderColors background="#0E4C5A" foreground="#FFFFFF"' +
  '    backgroundHover="#125F70" foregroundHover="#FFFFFF"' +
  '    backgroundPressed="#093B46" foregroundPressed="#FFFFFF"' +
  '    backgroundSelected="#0D4550" foregroundSelected="#FFFFFF" />' +
  '</CustomTheme>';

const r = await Xrm.WebApi.createRecord('webresource', {
  name: 'new_caldova_theme',
  displayname: 'Caldova Theme',
  webresourcetype: 4,
  content: btoa(unescape(encodeURIComponent(xml)))
});
```

Attributes you can set on `<CustomTheme>`:

| Attribute | Purpose |
|---|---|
| `basePaletteColor` | Seed hex for the generated Fluent 2 palette |
| `lockPrimary` | `true` = use the seed exactly; `false` = accessibility-optimised variant |
| `font` | Custom font family |
| `logoWebResource` | **Logical name** of an image web resource (recommended 156×48 px) |
| `logoTooltip` | Hover text on the logo |

### 2. Point the `CustomThemeDefinition` org setting at it

```js
await Xrm.WebApi.createRecord('organizationsetting', {
  'settingdefinitionid@odata.bind': '/settingdefinitions(<guid>)',
  value: 'new_caldova_theme'
});
```

Note the lookup bind is **lowercase** `settingdefinitionid@odata.bind`. The PascalCase form
fails with *"undeclared property"*.

### 3. Turn on the master switch ← the step that is not documented

```js
await Xrm.WebApi.createRecord('organizationsetting', {
  'settingdefinitionid@odata.bind': '/settingdefinitions(<guid>)',
  value: 'true'
});
```

`EnableDesignLanguageThemeSystem` **defaults to `false`** and is not called out prominently
anywhere. **Without it, steps 1 and 2 do absolutely nothing.** This cost real debugging time.

To resolve the setting definition IDs in your own environment:

```js
const r = await Xrm.WebApi.retrieveMultipleRecords('settingdefinition',
  "?$select=uniquename,settingdefinitionid&$filter=uniquename eq 'CustomThemeDefinition' or uniquename eq 'EnableDesignLanguageThemeSystem'");
console.table(r.entities);
```

### 4. Publish

```js
await fetch(Xrm.Utility.getGlobalContext().getClientUrl() + '/api/data/v9.2/PublishAllXml',
  {method:'POST', headers:{'Content-Type':'application/json','OData-Version':'4.0'}, body:'{}'});
```

Reload the app. Verify by sampling the header pixel colour rather than trusting your eyes —
Fluent 2 default `#021838` and a dark custom colour look similar in a screenshot.

---

## Header-only alternative

If you only want the header colour and not a full palette, use a bare `<AppHeaderColors …/>`
XML web resource and point the **`OverrideAppHeaderColor`** setting at it instead.

> `OverrideAppHeaderColor` is **ignored** when `CustomThemeDefinition` is set. Use one or the other.

---

## What modern theming does not cover

Per Microsoft's own documentation, some surfaces do not use modern theming yet: legacy grids,
row summaries, focus view, and the sales pipeline. The **app-selector page**
(`pagetype=apps`) also keeps its own fixed chrome — do not use it to verify your theme.
Check inside an actual app such as Sales Hub.

The **app icon** (tile image) is separate from theming entirely — set it per app in
App Designer → Settings → General → Icon.

---

## Gotcha: do not append junk query params to `main.aspx`

Adding a cache-buster like `&t=1787683030945` to a `main.aspx` URL throws an
**unhandled server exception** error page. Use the plain URL.
