# D10_FRONTEND_MIGRATION.md

**Phase 3.1 — D7 frontend audit and D7 → D10 mapping**
Project: CE Online Training Courses — Drupal 7.105 → Drupal 10.6.15
Produced: 2026-08-24 · **Status: AUDIT ONLY. No D10 theme created. No asset copied. No D7 file modified.**

**Governing rule**

> `cetc_new` is the **FRONTEND GOLDEN MASTER**.
> The D10 site must reproduce the current D7 frontend as closely as technically possible.
> **Technical rewrite is allowed. Visual redesign is not.**

Tags: **[P]** preserve · **[TR]** technical rewrite, same visual result · **[SF]** security fix, no visual
change · **[BL]** business logic currently living in the theme layer

Sources audited: `sites/all/themes/cetc_new` (179 files), `sites/all/themes/bootstrap`
(drupal/bootstrap **7.x-3.22**), and the live D7 database (`system`, `variable`, `block`, `filter`,
`views_view`, `views_display`, `ckeditor_settings`).

---

## 1. The stack that is actually running

```
cetc_new   theme_default = "cetc_new"   status = 1
  └─ base theme: bootstrap 7.x-3.22    status = 0  (normal for a D7 base theme)
```

### 1.1 ⚠️ The Bootstrap situation is a HYBRID — the single most important finding

| Layer | What actually runs | Where from |
|---|---|---|
| **Markup / CSS classes** | **Bootstrap 3** | `bootstrap` 7.x-3.22 PHP theme layer (`includes/*.inc`, `templates/*.tpl.php`) |
| **Grid CSS** | **Bootstrap 4.6.0** | `css/bootstrap-grid.css` (67 KB), loaded from `.info` |
| **Component JS** | **Bootstrap 3.3.7** | `js/bootstrap.js` (69 KB), loaded from `.info` |
| **Popper** | Popper 1.x (a Bootstrap **4** dependency) | `js/popper.js` (19 KB) — loaded, but BS 3.3.7 never calls it |
| **Base-theme Bootstrap CDN** | **Removed at runtime** | `settings[bootstrap_cdn]='Disabled'` **and** `cetc_new_css_alter()` / `cetc_new_js_alter()` explicitly `unset()` the `//cdn.jsdelivr.net/bootstrap/3.3.5/…` CSS and JS |
| `css/bootstrap.css` (BS **4.6.0** full, 195 KB) | **Present on disk, never loaded.** Referenced in no `.info`, `.php`, `.less` or `.css` | dead asset |

**Consequence:** the design is carried by `style.css` + `responsive.css` sitting over a **BS4.6 grid**,
while the DOM carries **BS3 class names**. **The CSS files are the golden master, not the framework.**
Any approach that swaps in a different Bootstrap version's CSS changes the rendering.

### 1.2 Asset load order (`cetc_new.info`, exact)

```
CSS   css/style.css                   152 KB   ← the actual design
      css/responsive.css               33 KB   ← 12 max-width breakpoints
      css/bootstrap-grid.css           67 KB   ← Bootstrap 4.6.0 grid only
      css/uicons-regular-rounded.css   18 KB
      css/font-awsome.css              72 KB   (filename misspelled in D7 — keep or rename deliberately)
      css/field.css

JS    js/popper.js       19 KB
      js/bootstrap.js    69 KB  (Bootstrap 3.3.7)
      js/libs.js        4.1 KB  (alert.js, collapse.js, transition.js, fancybox 1.3.4)
      js/table2excel.js 9.8 KB
      js/table2csv.js   3.5 KB
```

**Plus one script not in `.info`:** `js/script.js` (13 KB) is added by `cetc_new_preprocess_page()` via
`drupal_add_js(..., 'scope' => 'footer')`. The `.info` line is commented out. **It loads on every page.**

`less/` (35 files) is the LESS source for `style.css` (`style.less` imports reset → abstractions → core →
components → regions → pages → overrides). **Sync between `less/` and `css/style.css` is unverified — Q4.**

---

## 2. Theme regions

`cetc_new` declares **11**; the base theme declares 10.

| D7 region | Label | D10 `.info.yml` | Note |
|---|---|---|---|
| `navigation` | Navigation | `navigation` | **[P]** |
| `header` | **Header** | `header` | **[P]** base theme calls it "Top Bar"; `cetc_new` renames it |
| `highlighted` | Highlighted | `highlighted` | **[P]** own `region--highlighted.tpl.php` |
| `help` | Help | `help` | **[P]** |
| `content` | Content | `content` | **[P]** |
| `sidebar_first` | Primary | `sidebar_first` | **[P]** |
| `sidebar_second` | Secondary | `sidebar_second` | **[P]** |
| `footer` | Footer | `footer` | **[P]** own `region--footer.tpl.php` |
| **`footer_bottom`** | **Footer Bottom** | **`footer_bottom`** | **[P] exists ONLY in `cetc_new`. Must be declared explicitly** |
| `page_top` / `page_bottom` | — | required by core | |

**Block placement (live D7 `block` table): 10 enabled blocks in `cetc_new`** — `content` ×6, `header` ×1,
`navigation` ×1, `footer` ×1, `footer_bottom` ×1.
**No block uses PHP visibility** (site-wide: `0` ×167, `1` ×21, **zero** `2`) and no block body contains
`<?php`. Blocks migrate as configuration with no PHP-filter dependency. **[TR]**

---

## 3. `page.tpl.php` (147 lines) — the page skeleton

Two complete header/menu structures, not one:

```
<div class="inner_page-main">
  <header id="navbar" role="banner">
    <div class="navbar-header">
      <a class="logo pull-left" href="{front}"><img src="{logo}"></a>
      {{ page.header }}
    </div>
    <button class="popup-side-toggle"><span class="icon-bar">×3</button>   ← mobile hamburger
  </header>
  <div class="main-body-content">
    <div class="container-fluid">
      <div class="row">
        {title_prefix} <h1 class="title" id="page-title">{title}</h1> {title_suffix}
        {messages}
        {tabs → <div class="tabs">}
        {{ page.highlighted }}
        {{ page.content }}
```

then a **separate off-canvas panel**:

```
<div class="popup-side-menu">
  <div id="popup-header"> … logo + page.header + close button … </div>
  <div class="container"><div class="popup-mainmenu">
      <div class="navbar-collapse collapse pull-right">
        <nav role="navigation" id="custom_main_menu">{{ primary_nav }}</nav>
  </div></div>
  <div class="popup-footer">{{ page.footer }}</div>
```

**Load-bearing markup that must be reproduced exactly [P]:**
`inner_page-main` · `#navbar` · `navbar-header` · `logo pull-left` · `popup-side-toggle` · `icon-bar` ×3 ·
`main-body-content` · `container-fluid` · `row` · `#page-title.title` · `tabs` · `popup-side-menu` ·
`#popup-header` · `popup-mainmenu` · `navbar-collapse collapse pull-right` · `#custom_main_menu` ·
`popup-footer`

⚠️ `page.header` is **rendered twice** (main header and off-canvas). Duplicate block output is
**existing D7 behaviour — preserve it**, do not "fix" it.
⚠️ `sidebar_first`, `sidebar_second`, `help` and `navigation` are **declared but not printed** in
`page.tpl.php`. Declare them in D10 anyway; do not add output that D7 does not produce.

**D10:** `page.html.twig` + `page--front.html.twig`, `page--user.html.twig`,
`page--checkout--complete.html.twig`, `page--node--take.html.twig`,
`page--node--quiz-results.html.twig`, `page--node--quiz-results--view.html.twig`. All **[TR]**.

---

## 4. `html.tpl.php`

`html.tpl.php` → `html.html.twig` **[TR]**. Carries `<head>`, `$head_title`, body classes, `$page_top`,
`$page_bottom`, `$scripts`. **RDF attributes are dropped** — `rdf` is removed from D10 core (§10 Q9) and
produces no visual change.

---

## 5. Node templates (13)

| D7 | D10 Twig | Page it renders |
|---|---|---|
| `node--course.tpl.php` | `node--course--full.html.twig` | **Course detail** |
| `node--course--teaser.tpl.php` | `node--course--teaser.html.twig` | Course listing cards |
| `node--video.tpl.php` | `node--video--full.html.twig` | **Course video page** |
| `node--video--teaser.tpl.php` | `node--video--teaser.html.twig` | Video lists |
| `node--video--homepage-video.tpl.php` | `node--video--homepage-video.html.twig` | Homepage (custom view mode) |
| `node--29--homepage-video.tpl.php` | `node--29--homepage-video.html.twig` | ⚠️ **hard-coded nid 29** |
| `node--36--full.tpl.php` | `node--36--full.html.twig` | ⚠️ **hard-coded nid 36** |
| `node--certificate.tpl.php` | `node--certificate--full.html.twig` | **Certificate** |
| `node--blog.tpl.php` / `--teaser` | `node--blog--full/teaser.html.twig` | Blog |
| `node--page.tpl.php` | `node--page--full.html.twig` | Basic pages |
| `comment.tpl.php` | `comment.html.twig` | Comments |
| `field--field-course-image.tpl.php` | `field--field-course-image.html.twig` | Course image |
| `bkp-user-profile.tpl.php` | **do not port** | Backup copy, registered by no theme hook |

⚠️ **`node--29--…` and `node--36--full` make node-ID preservation a FRONTEND requirement.** If nid 29 or
36 changes during migration, two pages silently lose their design. Add to RECONCILIATION.md.

`cetc_new_preprocess_node()` adds the `node__TYPE__teaser` suggestion — in D10 core provides this
natively **[TR]**.

---

## 6. Block templates

`cetc_new` overrides **no** block template. `block.tpl.php` is **inherited from the base theme**, which
emits Bootstrap 3 block markup. **The D10 theme must reproduce that inherited markup**, not core's default
`block.html.twig`. This is a direct consequence of §1.1 and feeds decision A (§11).

---

## 7. Views templates and Views-specific rendering

### 7.1 Theme-level Views templates (9)

| D7 template | View | D10 |
|---|---|---|
| `views-view--evaluation-results.tpl.php` | `evaluation_results` | `views-view--evaluation-results.html.twig` |
| `views-view-table--evaluation-results.tpl.php` | `evaluation_results` | `views-view-table--evaluation-results.html.twig` |
| `views-view--report-history--page.tpl.php` | `report_history` | `views-view--report-history--page.html.twig` |
| `views-view-table--enrolled-courses--entity-view-1.tpl.php` | `enrolled_courses` (EVA display) | requires `eva` ^3.1 |
| `report/views-view--Report--panel-pane.tpl.php` | `Report` | ⚠️ Panels pane → Views page |
| `report/views-view--admin-report-new--panel-pane.tpl.php` | `admin_report_new` | ⚠️ Panels pane |
| `sale_report/views-view--sale-report--panel-pane-adv-report-detail.tpl.php` | `sale_report` | ⚠️ Panels pane |
| `views-bootstrap-grid-plugin-style.tpl.php` | any `bootstrap_grid` display | ⚠️ requires `views_bootstrap` |

### 7.2 Which views use which contrib plugin — measured from `views_display`

**`views_bootstrap` style plugins — 7 views, and four are CUSTOMER-FACING:**

| View | Plugin (display) |
|---|---|
| **`courses`** | `grid(page)` — **the main course listing at `/courses`** |
| **`courses_by_states`** | `grid(default)`, `grid(page)` — `/courses/%` |
| **`related_courses`** | `grid(block)`, `grid(block_1)` |
| **`states_link_block`** | `grid(block)` |
| `blog_cateogries` | `dropdown(block)` |
| `admin_coupon_used_by_user` | `table(page)` |
| `users_without_license_number` | `table(default)` |

**`better_exposed_filters` — 5 views, including the public course listing:**
`courses(page)` · `courses_by_states(page)` · `evaluation_results(default)` ·
`sale_report(5 displays)` · `admin_report_new(2 displays)`

**`views_aggregator`** — `sale_report` and `admin_report_new`, using `sum`, `first`, `enumerate`,
`group_and_compress`. **These produce financial report totals.**
**`views_merge_rows`** — `Report` (5 displays incl. 2 `views_data_export` displays).
**`views_autocomplete_filters`** — `Report`, `admin_report_new`, `evaluation_results`, `sale_report`.

### 7.3 ⚠️ Report pages are FRONT-END, not admin

Measured view page paths:

```
report1                        Report              → cetc_new
evaluation-results1            evaluation_results  → cetc_new
accounts-export                accounts_export     → cetc_new
quiz-certificate               quiz_certificate    → cetc_new
courses, courses/%             courses…            → cetc_new
user/%/orders                  commerce_user_orders→ cetc_new
admin/commerce/orders/list     commerce_orders     → seven
admin/commerce/orders/carts    commerce_orders     → seven
admin/users-without-license-number                 → seven
admin/admin-coupon-used-by-user                    → seven
```

**Business reporting renders in `cetc_new`, not the admin theme.** This scopes the admin-theme change
(see D10_ADMIN_THEME_MIGRATION.md) and it means the report templates are **golden-master pages**.

---

## 8. Form templates and form alters

`cetc_new` overrides no form `.tpl.php`; forms are styled by base-theme markup + `style.css`, and modified
by `cetc_new_form_alter()`. **Every item below is user-visible and must be reproduced [P]:**

| Form | D7 behaviour | D10 |
|---|---|---|
| `views_form_commerce_cart_form_default` | **Blanks the "delete" button label** (`#value = ''` — CSS supplies an icon); **hides** `actions.submit` (`#access = FALSE`); relabels checkout to **"Continue to checkout"** | `hook_form_alter` in `ce_course`/theme **[TR]**. ⚠️ Commerce 3 cart form ids differ |
| `views_exposed_form` — `views-exposed-form-courses-page` | Placeholder **"Search your course"** | **[P]** |
| `views_exposed_form` — `views-exposed-form-courses-by-states-page` | Placeholder **"Search your course"** | **[P]** |
| `views-exposed-form-Report-panel-pane-admin-report` | `date_filter` + `date_filter_1`: titles set `invisible`, placeholders **"Start Date" / "End Date"** | **[P]** |
| `views-exposed-form-admin-report-panel-pane-1` | same | **[P]** |
| `views-exposed-form-admin-report-new-panel-pane-admin-report-new-detail` | same | **[P]** |
| `views-exposed-form-sale-report-panel-pane-adv-report-new-detail` | Prepends `_fix_max_date_submit_function` | **[BL]** appends `23:59:59` to the max date so the last day is included. **A reporting rule, not styling** — reproduce exactly |
| `mailchimp_signup_subscribe_block_3_tips_…` | Placeholder **"Enter your email address"** | ⚠️ `mailchimp` currently deferred |
| `user_profile_form` | Custom `current_pass` description paragraph | **[P]** verbatim string |
| `user_pass_reset` | Replaces the message with a custom one using **`$user->realname`** and the expiry date; blanks `$form['help']` | **[P]** — depends on `realname` ^2.0 |

**Buttons — `cetc_new_preprocess_button()`:** `t('Add to cart')` and `t('Checkout')` are forced to
`class="btn btn-danger"`; everything else defers to `bootstrap_preprocess_button()`. **[P]**

---

## 9. `template.php` — 19 functions, 700 lines

⚠️ **~350 lines are business logic, not styling.** Those are **[BL]** and must leave the theme.

| # | Function | Lines | D10 home | Tag |
|---|---|---:|---|:--:|
| 1 | `cetc_new_preprocess_node()` | 8–53 | **[BL]** Queries `quiz_node_results`, checks the `bought` flag across course videos, `drupal_goto('<front>')` with an error when not purchased **or already passed**. Contains the hard-coded **`score >= 75`** (decision 13 — preserve, including the `>=` vs `>` inconsistency). → route access check / subscriber in `ce_course` | **BL / P** |
| 2 | `cetc_new_breadcrumb()` | 55–71 | Honours `bootstrap_breadcrumb`; emits `theme('item_list')` with `class="container"` → breadcrumb block + `breadcrumb.html.twig` | TR |
| 3 | `cetc_new_menu_tree__secondary()` | 74–76 | `<ul class="menu nav navbar-nav secondary col-md-9">` → `menu--secondary.html.twig` | TR |
| 4 | `cetc_new_preprocess_button()` | 79–95 | See §8 | TR |
| 5 | `cetc_new_menu_link()` | 98–103 | Footer menu links get `col-md-4 col-sm-6`; defers to `bootstrap_menu_link()` | TR |
| 6 | `cetc_new_form_alter()` | 106–167 | See §8 | TR/BL |
| 7 | `_fix_max_date_submit_function()` | 169–177 | **[BL]** max date + `23:59:59` | P |
| 8 | `cetc_new_form_user_pass_reset_alter()` | 183–191 | See §8 | TR |
| 9–13 | `preprocess_views_view*` ×5 | 196–265 | Views wrappers, field markup, **`sale_report` money formatting**, `field_profession_type` formatting on `Report` and `sale_report` | TR |
| 14 | `…_preprocess_commerce_abandoned_carts_email()` | 266–274 | Abandoned-cart email variables | TR |
| 15 | **`cetc_new_certificate_body_alter()`** | **275–566 (292 lines)** | ⚠️ **[BL] RISK_REGISTER R18 — the entire certificate is built here** (provider approval number, provider licence number, course number, course title, …). → **`ce_certificate` service**, decision 10. **Verified by rendered-PDF diff, not code review** | **BL / TR** |
| 16 | `cetc_new_preprocess_page()` | 567–635 | ⚠️ **[BL]** — see §9.1 | **BL** |
| 17 | `cetc_new_menu_link__main_menu()` | 637–688 | Main-menu markup | TR |
| 18 | `cetc_new_css_alter()` | 691–695 | Removes BS 3.3.5 CDN CSS | **obsolete in D10** |
| 19 | `cetc_new_js_alter()` | 696–700 | Removes BS 3.3.5 CDN JS | **obsolete in D10** |

### 9.1 `cetc_new_preprocess_page()` — five behaviours hidden in a preprocess hook

1. **Forced profile completion [BL].** Any logged-in user not already on `user/UID/edit` whose
   `field_social_security_number`, `field_cosmetology_credential_num`, `field_licensee_number`,
   `field_state_of_licensure`, `field_profession_type` **or** `field_address` is empty gets a
   `"Complete your Profile"` warning and **`drupal_goto('/user/UID/edit')` + `drupal_exit()`**.
   → request/route **event subscriber in `ce_users`**. This is a site-wide redirect, not styling.
2. **Post-login course return [BL].** Consumes `$_SESSION['course_url']` and redirects.
   → event subscriber + private tempstore in `ce_course`.
3. **Anonymous page titles.** "Login" / "Create new account" / "Request new password" for `user`,
   `user/login`, `user/register`, `user/password`. → route title callbacks **[TR]**.
4. **Dynamic course-listing titles [BL].** For `/courses` and `/courses/%`, loads taxonomy terms from the
   `field_state_tid` and `field_course_type_tid` **query parameters** and rebuilds the title
   (e.g. "Texas Cosmetology Courses"). **These titles are indexed content — any change is a defect.**
5. Adds `js/script.js` in the footer.

---

## 10. CSS, JavaScript, assets, responsive behaviour

### 10.1 CSS

Port `style.css` (152 KB), `responsive.css` (33 KB), `bootstrap-grid.css` (67 KB),
`uicons-regular-rounded.css`, `font-awsome.css`, `field.css` **byte-for-byte** into
`cetc_d10.libraries.yml`, in the load order of §1.2. **No re-authoring, no minification that alters
computed values.** `css/bootstrap.css` (dead) is **deliberately not ported**.

**Responsive contract — 12 breakpoints, all `max-width`:**
`1500 · 1300 · 1200 · 1100 · 1024 · 991 · 800 · 797 · 768 · 700 · 620 · 576`
⚠️ `797px`, `800px`, `991px`, `1100px`, `1300px`, `1500px` are **not** any framework's standard scale.
They exist only in `responsive.css`. **This is decisive evidence for §11: re-authoring onto another
framework's breakpoints would change mobile and tablet rendering.**
Mobile navigation is the `popup-side-toggle` → `popup-side-menu` off-canvas panel in `page.tpl.php` (§3),
driven by `script.js`/`libs.js`, **not** by Bootstrap's `navbar-collapse` behaviour alone.

### 10.2 JavaScript

| Asset | Contents | D10 |
|---|---|---|
| `bootstrap.js` (BS 3.3.7) | BS3 components | §11 decides the version |
| `popper.js` | Popper 1.x | **Unused by BS 3.3.7.** Do not port unless §11 selects a Bootstrap that needs it |
| `libs.js` + `js/libs/` | `alert.js`, `collapse.js`, `transition.js`, **fancybox 1.3.4** + `jquery.mousewheel-3.0.4` | Fancybox is **jQuery**-based. D10 loads no jQuery implicitly → declare `core/jquery`. Visual result must match |
| `script.js` | `Drupal.behaviors.initAddAllToCart` — enables/disables **"Add all to cart"** and **"Buy all / checkout"**, fires **`gtag_report_conversion()`** on click | **[TR]** → `Drupal.behaviors`. ⚠️ The gtag conversions are **analytics business behaviour** — same events, same payload |
| `table2excel.js`, `table2csv.js` | Client-side **table → Excel / CSV export** on report pages | **[P] user-facing feature.** `views_data_export` covers the server side; **both must survive** |

Every jQuery-dependent script must declare `core/jquery` + `core/drupal` in `libraries.yml`. **[TR]**

### 10.3 Images, fonts, icons — copy verbatim

| Group | Files | Size | Note |
|---|---:|---:|---|
| `images/` | 34 | 4.7 MB | `CE-logo.svg`, `footer_logo.svg/png`, `search.svg`, `address.svg`, `at.svg`, `mouse_sprite.svg`, photography |
| `webfonts/` | 18 | 3.1 MB | Font Awesome (`fa-solid-900`, `fa-regular-400`, `fa-brands-400`) + `uicons-regular-rounded` |
| `icons/` | 4 | 88 KB | ⚠️ **Bespoke `cetc` icon font** (eot/svg/ttf/woff) — cannot be regenerated |
| `icon/` | 1 | 20 KB | duplicate `uicons-regular-rounded.css` — resolve the duplicate path |
| `fonts/certificate-font/gv.ttf` | 1 | 44 KB | ⚠️ **The certificate typeface.** A substituted font changes every certificate |

**Typography, colours and spacing are preserved by copying the CSS and fonts unchanged.**

---

## 11. Decisions A–E

### A. Should D10 remain Bootstrap-based? — **YES, in markup terms.**

Not out of preference: the DOM that `style.css` targets is Bootstrap 3 markup emitted by the base theme,
and four customer-facing views render through `views_bootstrap` grid plugins. Removing Bootstrap means
re-authoring 185 KB of CSS — a redesign. **The obligation is to preserve the Bootstrap 3 markup contract
and the existing CSS, not to adopt "a Bootstrap theme".**

### B. Which D10 Bootstrap-compatible architecture?

| Option | Description | Visual risk | Verdict |
|---|---|---|---|
| **A — `drupal/bootstrap` 3.x** ✅ **CONFIRMED AVAILABLE** | Same base-theme lineage; `cetc_d10` becomes its sub-theme; BS3 markup reproduced by the same project | **Lowest** | ✅ **`drupal/bootstrap 3.40.0` resolves cleanly against `drupal/core 10.6.15`** (dependency probe, 2026-08-24). **RECOMMENDED** |
| **B — `core/starterkit` theme reproducing the markup** | Twig hand-written to emit the same BS3 classes; `style.css`, `responsive.css`, `bootstrap-grid.css`, `bootstrap.js`, fonts, images copied **byte-for-byte** | **Low** — CSS unchanged by construction; risk confined to DOM reproduction, which is diffable | **Recommended fallback, and recommended outright if A is unavailable** |
| **C — Bootstrap 5** (`bootstrap` 5.x, `bootstrap_barrio`, `radix`) | Modern Bootstrap | **HIGH** | ❌ BS5 changes class names (`.panel` gone, `data-bs-*`, no jQuery) **and** ships different CSS. With the 797/991 px breakpoints and 152 KB of bespoke CSS this is a redesign wearing a migration's clothes |
| **D — Olivero / Claro / any core theme** | Generic | **TOTAL** | ❌ Explicitly forbidden |

> **Recommendation: OPTION A.** `drupal/bootstrap` **3.40.0** is confirmed Drupal 10.6.15-compatible, so
> the Bootstrap 3 markup contract survives with the same base-theme project that produces it today.
> `cetc_d10` becomes its sub-theme, and `style.css` / `responsive.css` / `bootstrap-grid.css` / the fonts
> / the images are still copied **verbatim**. Option B remains the fallback if the 3.40 markup turns out
> to differ from 7.x-3.22 in practice — a DOM diff during Phase 3.4 decides that.

### C. Is `views_bootstrap` actually required? — **YES.**

Measured, not assumed: **7 views** use its style plugins, and **four are customer-facing** — `courses`
(the main `/courses` listing), `courses_by_states` (`/courses/%`), `related_courses`,
`states_link_block` — plus `blog_cateogries` and two admin tables. `cetc_new` even overrides
`views-bootstrap-grid-plugin-style.tpl.php`.

✅ **Resolved by the dependency probe (2026-08-24): `drupal/views_bootstrap` 3.12.0 is Drupal
10.6.15-compatible** and is the **Bootstrap 3** line — the correct match for `drupal/bootstrap` 3.40.0.

⚠️ **Correction to an earlier recommendation.** The constraint previously proposed was `^5.5`. That is
**wrong**: `views_bootstrap` 5.x is described by its maintainers as *"styles … to work with Bootstrap 5
components"*. Installing 5.x would emit Bootstrap 5 grid markup into a Bootstrap 3 design.
**The correct constraint is `^3.12`.**

**Its deferral must end at Phase 3.4.** It is currently absent from `composer.json` by approved decision;
that is correct for the foundation and wrong for 3.4.

### D. D7 Bootstrap dependencies needing D10 equivalents

`cetc_new` overrides only **2 of the base theme's 16** templates (`page.tpl.php`, `html.tpl.php`).
Everything else is inherited, so D10 must supply an equivalent for:

| D7 base-theme dependency | Needed in D10 |
|---|---|
| `block.tpl.php`, `node.tpl.php`, `views-view-table.tpl.php`, `search-block-form.tpl.php` | Twig templates emitting the same BS3 markup |
| `bootstrap-panel.tpl.php`, `bootstrap-modal.tpl.php`, `bootstrap-carousel.tpl.php` | Only if used — audit content before dropping |
| `includes/` — `alter.inc`, `cdn.inc`, `common.inc`, `deprecated.inc`, `icons.inc`, `pre-render.inc`, `process.inc`, `registry.inc` | The PHP theme layer that adds `form-control`, `btn`, table and breadcrumb classes |
| **Direct calls:** `bootstrap_preprocess_button()`, `bootstrap_menu_link()`, `theme_get_setting('bootstrap_breadcrumb')` | Must exist or be reimplemented |
| **Theme settings that shape markup:** `bootstrap_fluid_container=0` · `bootstrap_button_colorize=1` · `bootstrap_button_iconize=1` · `bootstrap_image_responsive=1` · `bootstrap_table_hover=1` · `bootstrap_table_striped=1` · `bootstrap_table_bordered=0` · `bootstrap_table_condensed=0` · `bootstrap_table_responsive=-1` · `bootstrap_breadcrumb=1` · `bootstrap_breadcrumb_home=0` | **Reproduce their OUTPUT**, not merely offer equivalent settings |
| `jquery_update` (base theme `simplytest_dependencies`) | D10 core jQuery; declare `core/jquery` per library |

### E. Visual behaviour that cannot be reproduced directly

| # | Item | Why | Mitigation |
|---|---|---|---|
| E1 | **Panels / Page Manager panes** | Panels is not carried to D10; 3 report templates target panes | Rebuild as Views pages / Layout Builder; enforce markup equivalence by DOM diff |
| E2 | **`page--checkout--complete.tpl.php`** | Commerce 3's checkout-complete route and pane structure differ from Commerce 1 | Re-target the template; **[TR]**, not a design change |
| E3 | **Quiz page templates** (`page--node--take`, `--quiz-results`, `--quiz-results--view`) | Coupled to Quiz module route names, unresolved pending decision 5 | Cannot be finalised before the Quiz spike closes |
| E4 | **Cart form alters** | Commerce 3 form ids differ from `views_form_commerce_cart_form_default` | Re-target; preserve blanked delete label, hidden submit, "Continue to checkout" |
| E5 | **RDF attributes in `html.tpl.php`/`node.tpl.php`** | `rdf` removed from D10 core | No visual change; drop or add contrib `rdf` — **Q9** |
| E6 | **`ckeditor_accordion`** | D7 runs **CKEditor 4** (CDN 4.4.0); D10 ships **CKEditor 5** — different plugin architecture; "Accordion" is in 1 profile | Content scan for accordion markup before Phase 3.4 |
| E7 | **Fancybox 1.3.4** | 2011-era jQuery plugin; may not work with D10's jQuery | Keep if it runs; otherwise a visually identical replacement |
| E8 | **Duplicate `page.header` render** | Deliberate D7 quirk (§3) | Preserve; do not "fix" |
| E9 | **Font Awesome / uicons duplication** (`icon/` vs `css/`) | Two copies of the same stylesheet | Consolidate path only, identical output |

---

## 12. Major-page mapping (visual golden master)

Compared **D7 vs D10** at desktop / tablet / mobile against the 12 breakpoints. **Any difference without a
documented technical reason is a migration defect.**

| # | Page | D7 templates + dependencies | D10 |
|---|---|---|---|
| 1 | **Homepage** | `page--front.tpl.php`, `node--29--homepage-video`, `node--video--homepage-video` | Twig equivalents; **nid 29 must survive** |
| 2 | **Login** | `page--user.tpl.php`; title set by `preprocess_page`; `email_registration` (login by email **or** username, `login_with_username=1`) | `page--user.html.twig` + title callback |
| 3 | **Registration** | `page--user.tpl.php`; `captcha`; title "Create new account" | as above |
| 4 | **Password reset** | `user_pass_reset` alter using `realname` | **[P]** custom message |
| 5 | **Course listing `/courses`** | `views_bootstrap` **grid**, `better_exposed_filters`, `node--course--teaser`, exposed placeholder "Search your course", **dynamic title from query params** | needs `views_bootstrap` + `better_exposed_filters` (**both deferred — restore at 3.4**) |
| 6 | **Course listing by state `/courses/%`** | same + `courses_by_states` | as above |
| 7 | **Course detail** | `node--course.tpl.php`, `field--field-course-image`, `related_courses` grid block | Twig + `views_bootstrap` |
| 8 | **Product / add to cart** | `preprocess_button` → `btn btn-danger`; `script.js` add-all-to-cart + gtag | **[P]** |
| 9 | **Cart** | `views_form_commerce_cart_form_default` alters (§8) | ⚠️ **E4** |
| 10 | **Checkout** | Commerce 1 panes + base-theme form markup | Commerce 3 panes; markup diff |
| 11 | **Checkout complete** | `page--checkout--complete.tpl.php` | ⚠️ **E2** |
| 12 | **User dashboard / account** | `user-profile.tpl.php`, `-category`, `-item`; `enrolled_courses` EVA table; `user/%/orders` | `eva` ^3.1; user Twig |
| 13 | **Course video page** | `node--video.tpl.php` + Wistia embed (unsigned — decision 2) | `ce_wistia` media source |
| 14 | **Evaluation / questionnaire** | Webform node 27, **component `cid=30` load-bearing** | `webform` ^6.3 |
| 15 | **Quiz** | `page--node--take.tpl.php`; access gated in `preprocess_node` (**≥75**) | ⚠️ **E3** |
| 16 | **Quiz results** | `page--node--quiz-results(.--view).tpl.php`, `quiz_certificate` view at `/quiz-certificate` | ⚠️ **E3** |
| 17 | **Certificate** | `node--certificate.tpl.php` + **`certificate_body_alter()` 292 lines** + `gv.ttf` | `ce_certificate` service; **rendered-PDF diff** |
| 18 | **Reports** (`report1`, `evaluation-results1`, `accounts-export`) | 3 Panels-pane templates, `views_aggregator` (sum/first/enumerate/group_and_compress), `views_merge_rows`, `views_autocomplete_filters`, date placeholders, `_fix_max_date_submit_function`, table2excel/table2csv | ⚠️ **E1**; **front-end theme, not admin** |

---

## 13. Open questions

| # | Question | Blocks |
|---|---|---|
| ~~**Q1**~~ | ✅ **ANSWERED 2026-08-24.** `drupal/bootstrap` **3.40.0** resolves against core 10.6.15. **Option A is viable and recommended.** Restore at Phase 3.4 with `drupal/views_bootstrap: ^3.12` (**not** `^5.5`) and `drupal/better_exposed_filters: ^7.1` (stable 7.1.3 — **no alpha required**) | *closed* |
| **Q2** | Admin theme — see **D10_ADMIN_THEME_MIGRATION.md** | Phase 3.2 |
| **Q3** | Legacy `cetc` theme is enabled (status 1) with 6 blocks, but `theme_default` is `cetc_new`. Decision 12 preserves it in the repo. **Must it render in D10, or is repository preservation enough?** | Scope |
| **Q4** | Is `css/style.css` in sync with `less/`? If not, **`style.css` is authoritative** | Asset porting |
| **Q5** | Confirm E2 and E4 (Commerce 3 route/form-id re-targeting) are treated as **[TR]**, not design changes | Phase 3.4 |
| **Q6** | Confirm E1 — Panels panes → Views pages with markup equivalence enforced by diff | Phase 3.4 |
| **Q7** | Confirm `views_bootstrap` **and** `better_exposed_filters` return at Phase 3.4 (§11 C — both are on the public course listing) | Phase 3.4 |
| **Q8** | Confirm `gtag_report_conversion()` must fire identically in D10 | Phase 3.4 |
| **Q9** | `rdf` is enabled in D7 and removed from D10 core. Reproduce via contrib, or drop? (no visual change either way) | Phase 3.2 |

---

## 14. What this audit did NOT do

- **No D10 theme created.** No `cetc_d10` directory, no `.info.yml`, no `.twig`, no `libraries.yml`.
- **No asset copied.** No CSS, JS, font or image moved into the D10 project.
- **No package installed.** `views_bootstrap`, `better_exposed_filters`, `ckeditor_accordion`, `libraries`
  remain deferred and absent from `composer.json`.
- **No business logic changed.** Every **[BL]** item above is recorded, not moved.
- **No D7 modification.** `course_actions.module` md5 `f9913edaad4e3feb434ee201ebb2f55c`, unchanged.
- No visual comparison run — D10 renders nothing yet.

---

**Read with:** D10_ADMIN_THEME_MIGRATION.md · D10_ARCHITECTURE.md §9 · RISK_REGISTER.md R18 ·
PHASE_3_IMPLEMENTATION_PLAN.md §15 · TEST_PLAN.md · CLAUDE.md §30, §42.

**Phase 3.4 begins only on explicit approval, and only after Q1 is answered.**
