# FRONTEND_CONVERSION_STATUS.md

## ✅ FRONTEND CONVERSION COMPLETE — as far as dependencies allow (2026-08-25)

| | Done | Blocked | Dead |
|---|---:|---:|---:|
| **Templates** (36 D7 `.tpl.php`) | **34 Twig** | 3 panel-pane | 1 (`bkp-user-profile`) |
| **`template.php` functions** (19) | **16** | 3 | — |

Everything not blocked by another migration is written. **All RUNTIME-UNVERIFIED.**

### 🛑 The three biggest finds of the whole frontend pass

**1. A site-wide access gate was hiding in a theme preprocess function.**
`cetc_new_preprocess_page()` force-redirects **every authenticated user with any of six empty
profile fields** to their own edit form, on **every page load, anywhere on the site**. They cannot
browse, buy or watch anything until all six are filled. Moved to
`ce_users\ProfileCompletionSubscriber` — a kernel request subscriber, since preprocess runs too late
to redirect cleanly and a business gate does not belong in a theme.
⚠️ `/user/logout` is **not** exempt, so an incomplete user is redirected before reaching it. That is
D7's real behaviour and is preserved.

**2. The post-evaluation redirect's consumer was in the same function** — it reads
`$_SESSION['course_url']`, **unsets it**, and redirects. That is the other half of rule 12 (C-8).
Both halves now share `CourseRedirectTarget`, and the single-use clear is preserved: without it a
learner would be bounced to the course on every page load, permanently.

**3. `js/script.js` DOES load — my earlier note was wrong and is corrected.**
`cetc_new.info` has it commented out, which is why I first recorded it as inert. But
`template.php:634` adds it at runtime in **footer** scope. It is 12,793 bytes and drives
`.popup-newsletter`, `.student-detail--popup`, `.popup-overlay` and `.views-table` behaviour —
omitting it would have broken the newsletter popup and the dashboard referral popup. Now declared
last in the `global` library, matching footer scope.

### 🛑 4th find — core's `d7_block` would have scrambled the entire layout

**Priority 2 (block layout migration) traced, and it contains a silent failure.**

D7 block placements are **not** exported to code — unlike the homepage, which lives in
`course_features/homepage/`. They exist only in the `block` table, so the migration cannot be
reviewed by reading files.

Core provides `d7_block`. Traced through its plugins:

| Plugin | Behaviour with `cetc_new` |
|---|---|
| `BlockTheme::transform()` | `cetc_new` isn't a D10 theme, **but it is D7's default theme**, so it returns the D10 default → **`cetc_d10`** ✅ |
| `BlockRegion::transform()` | fast path requires `source_theme == destination_theme`. `cetc_new != cetc_d10`, so it **falls back to the static map** — which has only `bartik` and `seven` → **`default_value: content`** 🛑 |

> **Every block — header logo, footer menus, navigation — would land in `content`, in a migration
> that reports SUCCESS.** Nothing errors. Nothing logs.

**Fixed by `ce_migrate/migrations/ce_block.yml`**, a derived migration with an explicit identity map
for all **11 regions**. It works precisely because `cetc_d10` deliberately kept D7's region
**machine names** unchanged — a decision made early in the theme scaffold that pays off here.

**Verified**: the map's 11 regions are **identical** to those declared in `cetc_d10.info.yml` —
none missing, none extra.

Also carried over: block **titles** (`<none>` → `label_display: 0`), **weights**, and **visibility**
(pages/roles). **PHP visibility is skipped** — `drupal/php` is in composer `conflict`, so a block
relying on it is omitted rather than made unconditionally visible; showing a block D7 hid would be
worse than omitting it.

**Two scripts written:**

- `scripts/reconcile/d7_blocks.sh` — read-only D7 inventory. B-4 lists any region **not** covered by
  the map (i.e. would hit the `content` fallback); B-5 lists PHP-visibility blocks that **will be
  missing** from D10 and need review.
- `scripts/frontend/verify_block_layout.sh` — post-migration check that **fails loudly** if every
  block ends up in `content`, and warns above 60%.

### Other behaviours preserved this pass

- **Breadcrumb is a three-state setting** (`0` never / `1` always / `2` **admin only**). At `2` the
  breadcrumb is invisible on every public page — likely the live behaviour.
- **Button styling matches on VISIBLE TEXT** (`Add to cart`, `Checkout`), not on an id or class.
  Rename a label and the styling silently stops applying. Classes **replace** and are ordered
  `['btn-danger', 'btn']` — danger first.
- **Footer menu items carry `col-md-4 col-sm-6`** — grid classes that make the footer multi-column.
- **Main menu**: level-0 dropdowns get a caret, deeper ones get `.dropdown-submenu` and **no** caret;
  and 🛑 **menu images are suppressed on the front page**, so the same menu renders differently on `/`.
- **Profession abbreviations** are an exact-match lookup with **no fallback** — an unlisted
  profession renders unabbreviated.
- **Password-reset form**: uses the **realname**, and **blanks core's help text**.
- **Abandoned-cart email** looks the customer up **by order email, not uid** — an unmatched email
  leaves the greeting as "Hey !".


**Per-artifact tracker for the D7 `cetc_new` → D10 `cetc_d10` theme rewrite.**
Updated 2026-08-25. Full audit: `D10_FRONTEND_MIGRATION.md`.

> **HARD REQUIREMENT — the public frontend must not change.**
> `cetc_new` is the visual golden master. Colors, typography, spacing, layout, header, footer,
> navigation, menus, buttons, forms, cards, branding, logos and responsive behaviour are preserved.
> **Claro is ADMIN ONLY and is never the frontend theme.**

---

## 1. Theme scaffolding — ✅ created, ⏳ not enabled

```
web/themes/custom/cetc_d10/
├── cetc_d10.info.yml        11 regions, machine names IDENTICAL to D7
├── cetc_d10.libraries.yml   5 CSS + 5 JS, D7 load order preserved
├── cetc_d10.theme           19 D7 functions mapped, none implemented yet
├── css/  js/  images/  icon/  icons/  fonts/  webfonts/
└── logo.png  favicon.ico  screenshot.png
```

**9.5 MB of design assets copied from D7 and verified byte-for-byte** (md5, 11/11 files):
`style.css` · `responsive.css` · `bootstrap-grid.css` · `uicons-regular-rounded.css` ·
`font-awsome.css` · `popper.js` · `bootstrap.js` · `libs.js` · `table2excel.js` · `table2csv.js` ·
`logo.png`. D7 was **read only**; nothing in it was modified.

Both YAML files parse. Every declared asset exists on disk.

### Load order is the contract

D7 `.info` order is reproduced exactly, with explicit weights so D10's aggregator cannot resequence:

```
style.css → responsive.css → bootstrap-grid.css → uicons-regular-rounded.css → font-awsome.css
popper.js → bootstrap.js → libs.js → table2excel.js → table2csv.js
```

`style.css` **must** load before `responsive.css` and `bootstrap-grid.css` — reordering changes
which rule wins at equal specificity, which changes the design.

### Three D7 quirks preserved deliberately

| Quirk | D7 behaviour | D10 treatment |
|---|---|---|
| `css/field.css` declared but **file does not exist** | 404s / skipped — no styles applied | **Not declared.** Identical observable output; declaring a missing file would error in D10. Classified EXISTING BUG, preserved not fixed |
| `js/script.js` **commented out** in `.info` | does not load | **Not declared.** File copied for fidelity |
| `css/bootstrap.css` (198 KB) present but **never declared** | does not load | **Not declared.** Bootstrap 3 comes from the base theme; `bootstrap-grid.css` adds the BS4.6 grid on top — the documented hybrid |

⚠️ **`settings[bootstrap_cdn] = 'Disabled'`** — all Bootstrap assets are served locally. The hybrid
(BS3 markup / BS4.6 grid CSS / BS3.3.7 JS) is reproduced by no CDN build, so this must stay off.

---

## 1b. ✅ VERIFIED LIVE — 2026-08-25

`cetc_d10` is **installed and is the frontend default**. Front page returns **HTTP 200**.

| Check | Result |
|---|---|
| All 5 stylesheets served | ✅ present in aggregate #2 (292,872 bytes) |
| Raw files fetchable | ✅ 200 for all five, byte counts match disk exactly |
| Load order | ✅ `style.css` @17,199 **precedes** `responsive.css` @45,082 |
| Library resolves | ✅ 5 CSS + 5 JS, declared order, all `group=100` (theme) |
| Remote assets | ✅ **none** — everything local, matching D7 |
| Page title markup | ✅ `<h1 class="title" id="page-title">` |
| Structural markup | ✅ `inner_page-main` · `#navbar` · `main-body-content` · `popup-side-menu` · `popup-side-toggle` |
| Block order in `content` | ✅ title −30 · messages −25 · tabs −20 · actions −10 (D7 sequence) |
| `system.theme:default` | `cetc_d10` |
| `system.theme:admin` | **claro** — admin only, never the frontend |

### 🛑 The Bootstrap CDN was ON by default and had to be disabled

`drupal/bootstrap` 8.x-3.x defaults `cdn_provider` to `jsdelivr`, so D10 was loading three remote
assets — including a **full Bootstrap 3.4.8 stylesheet the D7 site never loads**:

```
cdn.jsdelivr.net/.../entreprise7pro-bootstrap@3.4.8/dist/css/bootstrap.min.css
cdn.jsdelivr.net/.../drupal-bootstrap-styles@0.0.2/.../drupal-bootstrap.min.css
cdn.jsdelivr.net/.../entreprise7pro-bootstrap@3.4.8/dist/js/bootstrap.min.js
```

D7 sets `settings[bootstrap_cdn] = 'Disabled'`. The D10 equivalent is `cdn_provider: ''`
("None (compile locally)", `empty_value = ""` in the base theme's own plugin). **Applied** —
zero remote assets remain. Left on, it would have altered spacing, typography and component
styling site-wide.

### Two bugs found and fixed along the way

1. **Positive library weights → HTTP 500.** Drupal 10 rejects them outright. Weights removed;
   declaration order is now the ordering contract. **Do not reintroduce weights.**
2. **My own verification scripts gave a false "NOT SERVED" verdict.** Two defects:
   `grep -q` on a pipe under `set -o pipefail` — grep closes the pipe, `printf` takes SIGPIPE, and
   the pipeline reports failure **even on a match**; and `grep -c` returning exit 1 on zero, so
   `|| echo 0` emitted `0\n0`. Both fixed by grepping a **file** instead of a pipe.
   *A check that can fail while the thing it checks is fine is as dangerous as no check at all.*

---

## 2. Blocking dependency

✅ **RESOLVED 2026-08-25.** `drupal/bootstrap` **3.40.0** installed, plus its required
`jquery_ui`, `jquery_ui_draggable` and `jquery_ui_resizable` modules (jQuery UI left core after D7).
`composer.lock` 245 packages = the recorded 242 + exactly these 3. Core still pinned **10.6.15**.

ℹ️ `drush theme:install` initially failed with *"unmet module dependencies"* — Drush cannot enable
modules and install a theme in one transaction. Enabling the three modules first resolved it; no
theme change was needed.

---

## 2b. 🛑 THE HOMEPAGE IS A PAGE MANAGER (PANELS) PAGE — not a node, not a view

Measured 2026-08-25 through the read-only connection:

```
D7 variable site_frontpage = s:5:"front";          -> path "front", NOT a node
D7 menu_router path "front" -> page_callback   = page_manager_page_execute
                               access_callback = ctools_access_menu
D7 /front renders HTTP 200 anonymously, <title> = "CE Online Training Courses"
D7 /front body classes    = html front not-logged-in no-sidebars
views_view with path "front"  -> none
url_alias for "front"         -> none
```

So the D7 homepage is one of the **7 Page Manager pages** listed in
`MODULE_MAPPING.md` §163 (`homepage`, `people`, `report_page`, `sale_report_page`,
`report_history_page`, `advanced_report_page`, `evaluation_results_page`) plus 2 `panels_mini`.

**Approved disposition already exists — no new decision.** `MODULE_MAPPING.md` §55 puts `panels`
8.x-4.10 in class **D**: *"7 displays + 4 Page Manager pages: rebuild as Layout Builder or Views
pages"*, and `PHASE_2_MIGRATION_DECISIONS.md` §261 agrees.

### Consequence for the front-page config

`system.site:page.front` is still the installer default `/user/login`, so **D10 currently opens on
the login form**. It cannot simply be switched:

> **Setting `page.front` to `/front` before the homepage Panels page is rebuilt produces a 404
> homepage.** The dependency is the Page Manager page — **not** the node migration, as I first
> assumed and have corrected.

Order: rebuild the `homepage` page (Layout Builder or Views) → set `page.front` → re-run the body
class diff, at which point `front` / `not-front` will align on every page.

---

## 2c. Block layout is still INSTALLER DEFAULTS

Installing `cetc_d10` made Drupal auto-create a default block set. Two landed in `sidebar_first`
(`cetc_d10_admin`, `cetc_d10_tools`), which is why the diff reports `one-sidebar sidebar-first`
where D7 has `no-sidebars`. **The class generator is correct; the block content is not.**

⚠️ **The whole block layout must come from migrating D7's `block` table.** Until then any D10↔D7
layout comparison is measuring placeholder configuration. Disabling the two sidebar blocks
(task 23) removes one known-wrong difference so the rest of the diff means something — it does not
make block layout done.

---

## 3. Templates — 36 `.tpl.php` → Twig

### ✅ Converted — 11 of 36 (all RUNTIME-UNVERIFIED: D10 has 0 nodes)

| D10 Twig | From | Notes |
|---|---|---|
| `html.html.twig` | `html.tpl.php` | tracking stack, fonts, skip link |
| `page.html.twig` | `page.tpl.php` | 11 regions, off-canvas menu |
| `page-title.html.twig` | *(extracted)* | restores `<h1 class="title" id="page-title">` |
| `region--footer` · `region--highlighted` | same | override the **bootstrap base theme**, not core |
| `node--course--full` | `node--course.tpl.php` | **defects D-1…D-4** |
| `node--course--teaser` | same | hard-coded CE label |
| `node--video--full` | `node--video.tpl.php` | `?course_nid` gate |
| `node--video--teaser` | same | **4-way flag state machine + RB-12b `arg(1)`** |
| `node--blog--full` · `node--page--full` · `node--blog--teaser` | same | `realname` byline, `m/d/Y` |

Full conversion record incl. all preserved defects: **`COURSE_TEMPLATE_CONVERSION.md`**.

### 🛑 PRESERVED DEFECT P-1 — viewer / profile-owner confusion in `user-profile.tpl.php`

**Not in any project document. Found during conversion 2026-08-25.**

D7's user dashboard mixes two different users in one template:

```
$user                 = the GLOBAL user  = whoever is LOOKING     -> used 12x
$elements['#account'] = the profile being VIEWED                   -> used  2x
```

When an administrator opens **another** user's profile, the page shows:

| Element | Whose data | Correct? |
|---|---|:--:|
| name, email | the viewed user | ✅ |
| referral link | **the viewer** | ❌ |
| certificates list | **the viewer** | ❌ |
| "Edit" button | **the viewer's** account | ❌ |
| "My Orders" link | **the viewer's** orders | ❌ |
| which tabs appear | **the viewer's** roles | ❌ |

The **only** correct comparison in the whole file is the instructor section:
`if ($user->uid == $elements['#account']->uid && in_array('Instructor', $user->roles))`.

**Reproduced exactly.** Every `current_user_*` variable in `cetc_d10_preprocess_user()` is
deliberately the viewer. Repairing it would change what an admin sees on every profile page —
a UX and arguably an information-exposure change requiring explicit approval.

⚠️ **Flagged for a post-migration decision**, alongside certificate defects C-1/C-2. It is the
most consequential undocumented defect found so far, because it touches what data appears on a
page about *someone else*.

### Other preserved oddities in this group

- **Tab-state hack** — when the admin tab exists, the profile tab is given the *non-Bootstrap*
  classes `not-admin-dashboard` / `not-admin-in`, which do nothing, so it is not active. Those
  class names are load-bearing.
- **`user-profile-item`** prints the *same* `attributes` on both `<dt>` and `<dd>`, duplicating any
  id — invalid HTML, reproduced.
- **`user-profile-category` / `-item`** came from D7 core's **Profile module, removed in D8**.
  They are very likely inert in D10. Kept anyway (§30).
- **`page--user`** puts **messages before the title**, unlike `page.tpl.php`. Block weights must be
  `messages(−35) < title(−30) < tabs(−20)` on user pages.
- The certificates list order comes from `array_unique()` over a **score-ordered** result set —
  a single `GROUP BY` would order them differently. The two-query shape is preserved.

---

### 🛑 `bkp-user-profile.tpl.php` — DEAD CODE, deliberately not converted

I flagged this earlier as *"looks like an abandoned backup — must be checked for an active theme
suggestion before being treated as dead."* **That check is now done.**

| Evidence | Result |
|---|---|
| Theme hook `bkp_user_profile` defined anywhere | **none** |
| Suggestion `user_profile__bkp` or similar | **none** |
| Referenced by any module or template | **no** — only its own file matches |
| Content | a **different, older** profile design — 146 lines vs `user-profile`'s 237, no tab structure |

Drupal cannot invoke it. There is no hook, no suggestion, and nothing references it. It is a manual
backup file someone left in the templates directory.

**Not converted.** A Twig file that can never render adds noise and implies a behaviour that does not
exist. This is **not** retiring theme code under §30 — **the D7 file is untouched**, and the
decision reverses in minutes if evidence of a hook ever appears.

### 🛑 PRESERVED DEFECT M-1 — the abandoned-cart email uses two different domains

```
cart link  ->  https://ceonlinetrainingcourses.com/checkout
signature  ->  https://ceonlinetraining.com
```

Both appear across the D7 codebase (`ceonlinetrainingcourses.com` 8×, `ceonlinetraining.com` 5×), so
neither is obviously the typo — but a customer receiving this email sees **two different addresses
for the same business**, and only one can be the live storefront.

⚠️ **If the cart-link domain is dead or unparked, the "return to your cart" link in every
abandoned-cart email is broken today.** That is a live revenue issue, not a migration one.

Reproduced exactly and **flagged for the business owner**. Deliberately not normalised: choosing
which domain is authoritative is a business decision, not a conversion detail.

Also noted: this email signs off as **"CE Training Courses, Inc."** while the certificate says
**"CE Online Training Courses, Inc."** Both preserved in their own templates.

---

### ⏳ Remaining — 25

| Group | Templates |
|---|---|
| **Certificate** | `node--certificate` — pairs with the ~290-line `certificate_body_alter()` extraction |
| **Checkout / quiz pages** | `page--checkout--complete` · `page--node--take` · `page--node--quiz-results` · `page--node--quiz-results--view` · `page--front` · `page--user` |
| **User profile** | `user-profile` · `user-profile-category` · `user-profile-item` · `bkp-user-profile` |
| **Views (9)** | `views-view--Report--panel-pane` · `views-view--admin-report-new--panel-pane` · `views-view--sale-report--panel-pane-adv-report-detail` · `views-view--evaluation-results` · `views-view--report-history--page` · `views-view-table--enrolled-courses--entity-view-1` · `views-view-table--evaluation-results` · `views-bootstrap-grid-plugin-style` |
| **Hard-coded nid** | `node--29--homepage-video` · `node--36--full` · `node--video--homepage-video` |
| **Misc** | `comment` · `field--field-course-image` · `commerce_abandoned_carts_email` |

⚠️ Three of the Views templates are **`panel-pane` displays** — they belong to the Page Manager /
Panels pages covered in §2b, so they cannot be finished before those pages are rebuilt.

---

### Original risk grouping

| Group | Count | Templates |
|---|---:|---|
| **Page-level** | 8 | `html` · `page` · `page--front` · `page--user` · `page--checkout--complete` · `page--node--take` · `page--node--quiz-results` · `page--node--quiz-results--view` |
| **Node-level** | 12 | `node--course` · `node--course--teaser` · `node--video` · `node--video--teaser` · `node--video--homepage-video` · `node--blog` · `node--blog--teaser` · `node--page` · `node--certificate` · `node--29--homepage-video` · `node--36--full` · `field--field-course-image` |
| **Views** | 7 | `views-view--evaluation-results` · `views-view--report-history--page` · `views-view-table--enrolled-courses--entity-view-1` · `views-view-table--evaluation-results` · `views-bootstrap-grid-plugin-style` · plus `report/` and `sale_report/` subdirectories |
| **User / profile** | 4 | `user-profile` · `user-profile-category` · `user-profile-item` · `bkp-user-profile` |
| **Region / misc** | 5 | `region--footer` · `region--highlighted` · `comment` · `commerce_abandoned_carts_email` · *(node--certificate counted above)* |

⚠️ **`bkp-user-profile.tpl.php`** looks like an abandoned backup. It must be **checked for an active
theme suggestion before being treated as dead** — `CLAUDE.md` §30 forbids retiring theme code merely
to reduce scope. Not deleted.

⚠️ **Numeric-nid templates** — `node--29--homepage-video` and `node--36--full` hard-code node IDs
29 and 36. **Node IDs must survive the migration for these to keep matching.** If nid remapping
occurs, these templates silently stop applying and two pages change appearance. Flagged for the node
migration.

---

## 4. `template.php` → `cetc_d10.theme`

19 functions mapped in the `.theme` docblock; **none implemented**. 8 carry business logic `[BL]`
that must move out of the theme layer while producing identical output.

🛑 **`cetc_new_certificate_body_alter()` is ~290 lines** (template.php 275–566) of certificate
generation living in the theme. It belongs in `ce_certificate` (`CLAUDE.md` §21 — technical rewrite,
observable behaviour equivalent). Full spec: **`CERTIFICATE_EXTRACTION.md`**.

> ⚠️ **CORRECTION 2026-08-25.** An earlier revision of this file said the certificate function
> carries the hard-coded **75** pass mark. **It does not.** A complete read found no score,
> pass-mark, percentage, CE/non-CE or taxonomy conditional anywhere in it. The 75 defect (D-1) lives
> **only** in `node--course.tpl.php`. Certificate eligibility is decided by the `quiz_certificate`
> **view** returning rows, or by the `completed` **flag** — never by a score comparison.

---

## 5. Verification method (per `CLAUDE.md` §32)

Nothing is "done" on assertion. Each converted page is compared against live D7 at the 12 measured
breakpoints — 1500 · 1300 · 1200 · 1100 · 1024 · 991 · 800 · 797 · 768 · 700 · 620 · 576 — across
the 18-page golden-master matrix in `D10_FRONTEND_MIGRATION.md`.

**No page is signed off from a screenshot of D10 alone.** D7 and D10 must be viewed side by side.

---

## Frontend coverage audit — 2026-08-25

`scripts/reconcile/frontend_audit.py`. Run offline, files only.

### Result after fixes

| asset | D7 | D10 |
|---|---|---|
| stylesheets | 9 | **9** |
| javascript | 16 | **16** |
| LESS sources | 35 | **35** |
| images (png/jpg/svg) | 29 / 4 / 26 | **29 / 4 / 26** |
| fonts (ttf/woff) | 5 / 9 | **5 / 9** |

All 12 behaviour markers present.

### 🛑 The audit's first version was wrong, and the way it was wrong matters

It diffed `cetc_new_*` function names against `cetc_d10_*` and reported **eight missing theme functions** — `breadcrumb`, `menu_link`, `menu_link__main_menu`, `menu_tree__secondary`, `preprocess_button`, and three views preprocess variants.

**All eight were false alarms.** Every one is implemented, under a Drupal-10-native name or as a Twig template:

| D7 | D10 |
|---|---|
| `cetc_new_breadcrumb` | `cetc_d10_preprocess_breadcrumb` |
| `cetc_new_menu_link` | `cetc_d10_preprocess_menu__footer` |
| `cetc_new_menu_link__main_menu` | `menu--main.html.twig` + `preprocess_menu` |
| `cetc_new_preprocess_button` | `cetc_d10_preprocess_input` |

Comparing **function names** across two theme architectures is the wrong test: D10 deliberately moves theme functions into Twig, so **a correct conversion looks like a missing function**. The script now probes for the markers those functions *produce* — the classes, wrappers and literal strings that would actually vanish.

Had I acted on the first result, I would have re-implemented eight functions that already worked, most likely duplicating behaviour that then fires twice.

### Two genuine gaps, both fixed

**1. All 35 LESS source files were absent.** `gruntfile.js` compiles them into the theme CSS; only the compiled output had been carried over.

**CLAUDE.md §30:** *"Do not retire/delete historical theme code merely to reduce scope without explicit approval."*

The loss is invisible — the site renders identically from compiled CSS. It surfaces the first time someone needs to change a colour and finds no source to change. Now preserved under `less/`, with `gruntfile.js` and `package.json`.

🛑 **They are not wired into the build.** The theme loads the committed CSS, byte-identical to D7's. Recompiling could regenerate CSS from sources that have drifted from the shipped output, and any difference is a silent visual change. **If ever regenerated, diff against the committed CSS first — a non-empty diff is a finding.**

**2. `ckeditor.styles.js` was absent.** `ckeditor.lib.inc:939` uses the theme's copy when the profile's `css_style` is `'theme'` — it populates the **Style drop-down content authors use**.

Whether it is active depends on a setting only the D7 database holds, so `d7_config_export.sh` **X-10** now captures it. The file is preserved so the definitions are not lost while the decision is open.

⚠️ It cannot simply be ported: CKEditor 4 reads a JS `stylesSet`; CKEditor 5 takes `ckeditor5_style` config on the text format. That is a **translation**, and it affects **authoring only** — no visitor sees a difference.

### What the audit cannot tell you

Markers prove **presence, not correctness**. A class in a comment counts as found. Only a rendered golden-master comparison against D7 proves the UI matches — and that needs the runtime.
