diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml
index ace8d2d2a..35b9a772f 100644
--- a/.github/workflows/acceptance.yml
+++ b/.github/workflows/acceptance.yml
@@ -153,7 +153,7 @@ jobs:
 
       - name: Initialise moodle-plugin-ci
         run: |
-          composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4.4
+          composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4.5
           echo $(cd ci/bin; pwd) >> $GITHUB_PATH
           echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH
           sudo locale-gen en_AU.UTF-8
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index d2397ede2..a176115c2 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -69,7 +69,8 @@ jobs:
         cat circular-dependencies
         lines=$(cat circular-dependencies | wc -l)
         echo "Total circular dependencies: $lines"
-        test $lines -le 185
+        test $lines -ge 138
+        test $lines -le 148
     - name: JavaScript code compatibility
       run: |
         npx check-es-compat www/*.js --polyfills="\{Array,String,TypedArray\}.prototype.at,Object.hasOwn"
diff --git a/local_moodleappbehat/templates/mobile.mustache b/local_moodleappbehat/templates/mobile.mustache
index d56f2fe4f..c73bd7fcd 100644
--- a/local_moodleappbehat/templates/mobile.mustache
+++ b/local_moodleappbehat/templates/mobile.mustache
@@ -3,8 +3,7 @@
 
 <ion-list>
     <ion-item>
-        <ion-label position="floating">What is the answer to the Ultimate Question of Life, The Universe, and Everything?</ion-label>
-        <ion-input [(ngModel)]="CONTENT_OTHERDATA.answer"></ion-input>
+        <ion-input labelPlacement="floating" [(ngModel)]="CONTENT_OTHERDATA.answer" label="What is the answer to the Ultimate Question of Life, The Universe, and Everything?"></ion-input>
     </ion-item>
     <ion-item *ngIf="CONTENT_OTHERDATA.answer === '42'">
         <ion-label>That is correct!</ion-label>
diff --git a/package-lock.json b/package-lock.json
index 282ddf4a3..8c296ef5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,7 +38,7 @@
                 "@awesome-cordova-plugins/sqlite": "^6.7.0",
                 "@awesome-cordova-plugins/status-bar": "^6.7.0",
                 "@awesome-cordova-plugins/web-intent": "^6.7.0",
-                "@ionic/angular": "^7.8.6",
+                "@ionic/angular": "^8.2.5",
                 "@ionic/cordova-builders": "^11.0.0",
                 "@moodlehq/cordova-plugin-advanced-http": "3.3.1-moodle.1",
                 "@moodlehq/cordova-plugin-camera": "7.0.0-moodle.1",
@@ -107,7 +107,7 @@
                 "@angular/cli": "^17.3.6",
                 "@angular/compiler-cli": "^17.3.7",
                 "@angular/language-service": "^17.3.7",
-                "@ionic/angular-toolkit": "^10.1.1",
+                "@ionic/angular-toolkit": "^11.0.1",
                 "@ionic/cli": "^7.2.0",
                 "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
                 "@types/faker": "^5.5.9",
@@ -3646,123 +3646,32 @@
             "dev": true
         },
         "node_modules/@ionic/angular": {
-            "version": "7.8.6",
-            "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.8.6.tgz",
-            "integrity": "sha512-3Qe53hXpyjtx6fFcxt/NTAlauIawsGmCZJPauV5sAnSKVuX8C82C1zMAZTeJt6m2dnd71wythc98BXUXsx/UxQ==",
+            "version": "8.2.5",
+            "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.2.5.tgz",
+            "integrity": "sha512-vvL5TIN8YbrkW5IZ4TYw2zVa4/+boITe19nElPz1Bu7O15lEEzLe+9RqcIMDERwzgqzsBXLh1CUJk+1TXkMhJg==",
             "dependencies": {
-                "@ionic/core": "7.8.6",
+                "@ionic/core": "8.2.5",
                 "ionicons": "^7.0.0",
                 "jsonc-parser": "^3.0.0",
                 "tslib": "^2.3.0"
             },
             "peerDependencies": {
-                "@angular/core": ">=14.0.0",
-                "@angular/forms": ">=14.0.0",
-                "@angular/router": ">=14.0.0",
+                "@angular/core": ">=16.0.0",
+                "@angular/forms": ">=16.0.0",
+                "@angular/router": ">=16.0.0",
                 "rxjs": ">=7.5.0",
-                "zone.js": ">=0.11.0"
+                "zone.js": ">=0.13.0"
             }
         },
         "node_modules/@ionic/angular-toolkit": {
-            "version": "10.1.1",
-            "resolved": "https://registry.npmjs.org/@ionic/angular-toolkit/-/angular-toolkit-10.1.1.tgz",
-            "integrity": "sha512-idLaBUY14M7JQmvxAGeDZvk7WcamWEHo1OHGRuLRAn+7uWrKeGxfWbnbZJhvRCLQndr8j7q3WV3Z+0APkPuKaQ==",
+            "version": "11.0.1",
+            "resolved": "https://registry.npmjs.org/@ionic/angular-toolkit/-/angular-toolkit-11.0.1.tgz",
+            "integrity": "sha512-dxx2RDbxDYM2nWRPIirKMJySHtqJ1u02T25PGbNb99W2Wlcmu1cza3+2/PQ8ga18yMz/dQqaGyEmPDf3ZSVO0w==",
             "dev": true,
             "dependencies": {
-                "@angular-devkit/core": "^16.0.0",
-                "@angular-devkit/schematics": "^16.0.0",
-                "@schematics/angular": "^16.0.0"
-            }
-        },
-        "node_modules/@ionic/angular-toolkit/node_modules/@angular-devkit/core": {
-            "version": "16.2.14",
-            "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.14.tgz",
-            "integrity": "sha512-Ui14/d2+p7lnmXlK/AX2ieQEGInBV75lonNtPQgwrYgskF8ufCuN0DyVZQUy9fJDkC+xQxbJyYrby/BS0R0e7w==",
-            "dev": true,
-            "dependencies": {
-                "ajv": "8.12.0",
-                "ajv-formats": "2.1.1",
-                "jsonc-parser": "3.2.0",
-                "picomatch": "2.3.1",
-                "rxjs": "7.8.1",
-                "source-map": "0.7.4"
-            },
-            "engines": {
-                "node": "^16.14.0 || >=18.10.0",
-                "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
-                "yarn": ">= 1.13.0"
-            },
-            "peerDependencies": {
-                "chokidar": "^3.5.2"
-            },
-            "peerDependenciesMeta": {
-                "chokidar": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/@ionic/angular-toolkit/node_modules/@angular-devkit/schematics": {
-            "version": "16.2.14",
-            "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.14.tgz",
-            "integrity": "sha512-B6LQKInCT8w5zx5Pbroext5eFFRTCJdTwHN8GhcVS8IeKCnkeqVTQLjB4lBUg7LEm8Y7UHXwzrVxmk+f+MBXhw==",
-            "dev": true,
-            "dependencies": {
-                "@angular-devkit/core": "16.2.14",
-                "jsonc-parser": "3.2.0",
-                "magic-string": "0.30.1",
-                "ora": "5.4.1",
-                "rxjs": "7.8.1"
-            },
-            "engines": {
-                "node": "^16.14.0 || >=18.10.0",
-                "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
-                "yarn": ">= 1.13.0"
-            }
-        },
-        "node_modules/@ionic/angular-toolkit/node_modules/@schematics/angular": {
-            "version": "16.2.14",
-            "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.14.tgz",
-            "integrity": "sha512-YqIv727l9Qze8/OL6H9mBHc2jVXzAGRNBYnxYWqWhLbfvuVbbldo6NNIIjgv6lrl2LJSdPAAMNOD5m/f6210ug==",
-            "dev": true,
-            "dependencies": {
-                "@angular-devkit/core": "16.2.14",
-                "@angular-devkit/schematics": "16.2.14",
-                "jsonc-parser": "3.2.0"
-            },
-            "engines": {
-                "node": "^16.14.0 || >=18.10.0",
-                "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
-                "yarn": ">= 1.13.0"
-            }
-        },
-        "node_modules/@ionic/angular-toolkit/node_modules/jsonc-parser": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
-            "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
-            "dev": true
-        },
-        "node_modules/@ionic/angular-toolkit/node_modules/magic-string": {
-            "version": "0.30.1",
-            "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz",
-            "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==",
-            "dev": true,
-            "dependencies": {
-                "@jridgewell/sourcemap-codec": "^1.4.15"
-            },
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@ionic/angular-toolkit/node_modules/picomatch": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-            "dev": true,
-            "engines": {
-                "node": ">=8.6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/jonschlinkert"
+                "@angular-devkit/core": "^17.0.0",
+                "@angular-devkit/schematics": "^17.0.0",
+                "@schematics/angular": "^17.0.0"
             }
         },
         "node_modules/@ionic/angular/node_modules/jsonc-parser": {
@@ -4291,11 +4200,11 @@
             }
         },
         "node_modules/@ionic/core": {
-            "version": "7.8.6",
-            "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.8.6.tgz",
-            "integrity": "sha512-HAYZdEmeJgOdo2kDlZkcCGHb+zs/vjU6iv4skbVBL7y+OnSv/oC2u83Yee8S3/aY0YAxkyBgu7hLTYH13Zc2Aw==",
+            "version": "8.2.5",
+            "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.2.5.tgz",
+            "integrity": "sha512-NhK5KfP5NL5NITibj8sOUlfI/ARNCF5rBu5HdIEfFe25MJkd0IYBQWjVaESFhSk7aB8pXEP8DIx1AHbT9e3Sog==",
             "dependencies": {
-                "@stencil/core": "^4.12.2",
+                "@stencil/core": "^4.19.2",
                 "ionicons": "^7.2.2",
                 "tslib": "^2.1.0"
             }
diff --git a/package.json b/package.json
index 6d7090a34..b3d280a85 100644
--- a/package.json
+++ b/package.json
@@ -72,7 +72,7 @@
         "@awesome-cordova-plugins/sqlite": "^6.7.0",
         "@awesome-cordova-plugins/status-bar": "^6.7.0",
         "@awesome-cordova-plugins/web-intent": "^6.7.0",
-        "@ionic/angular": "^7.8.6",
+        "@ionic/angular": "^8.2.5",
         "@ionic/cordova-builders": "^11.0.0",
         "@moodlehq/cordova-plugin-advanced-http": "3.3.1-moodle.1",
         "@moodlehq/cordova-plugin-camera": "7.0.0-moodle.1",
@@ -141,7 +141,7 @@
         "@angular/cli": "^17.3.6",
         "@angular/compiler-cli": "^17.3.7",
         "@angular/language-service": "^17.3.7",
-        "@ionic/angular-toolkit": "^10.1.1",
+        "@ionic/angular-toolkit": "^11.0.1",
         "@ionic/cli": "^7.2.0",
         "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
         "@types/faker": "^5.5.9",
diff --git a/patches/@ionic+core+7.8.6.patch b/patches/@ionic+core+8.2.5.patch
similarity index 95%
rename from patches/@ionic+core+7.8.6.patch
rename to patches/@ionic+core+8.2.5.patch
index b88976d43..26270e1c8 100644
--- a/patches/@ionic+core+7.8.6.patch
+++ b/patches/@ionic+core+8.2.5.patch
@@ -1,5 +1,5 @@
 diff --git a/node_modules/@ionic/core/components/popover.js b/node_modules/@ionic/core/components/popover.js
-index 21fb3e3..52ea4a6 100644
+index 19b79c4..67289f4 100644
 --- a/node_modules/@ionic/core/components/popover.js
 +++ b/node_modules/@ionic/core/components/popover.js
 @@ -763,8 +763,10 @@ const iosEnterAnimation = (baseEl, opts) => {
@@ -29,10 +29,10 @@ index 21fb3e3..52ea4a6 100644
      const contentEl = root.querySelector('.popover-content');
      const referenceSizeEl = trigger || ((_a = ev === null || ev === void 0 ? void 0 : ev.detail) === null || _a === void 0 ? void 0 : _a.ionShadowTarget) || (ev === null || ev === void 0 ? void 0 : ev.target);
 diff --git a/node_modules/@ionic/core/dist/cjs/ion-popover.cjs.entry.js b/node_modules/@ionic/core/dist/cjs/ion-popover.cjs.entry.js
-index 68a908b..050e544 100644
+index 2dcf484..54aeac9 100644
 --- a/node_modules/@ionic/core/dist/cjs/ion-popover.cjs.entry.js
 +++ b/node_modules/@ionic/core/dist/cjs/ion-popover.cjs.entry.js
-@@ -768,8 +768,10 @@ const iosEnterAnimation = (baseEl, opts) => {
+@@ -769,8 +769,10 @@ const iosEnterAnimation = (baseEl, opts) => {
      const { event: ev, size, trigger, reference, side, align } = opts;
      const doc = baseEl.ownerDocument;
      const isRTL = doc.dir === 'rtl';
@@ -45,7 +45,7 @@ index 68a908b..050e544 100644
      const root = helpers.getElementRoot(baseEl);
      const contentEl = root.querySelector('.popover-content');
      const arrowEl = root.querySelector('.popover-arrow');
-@@ -889,8 +891,10 @@ const mdEnterAnimation = (baseEl, opts) => {
+@@ -890,8 +892,10 @@ const mdEnterAnimation = (baseEl, opts) => {
      const { event: ev, size, trigger, reference, side, align } = opts;
      const doc = baseEl.ownerDocument;
      const isRTL = doc.dir === 'rtl';
@@ -93,10 +93,10 @@ index 603923a..ff10a25 100644
      const contentEl = root.querySelector('.popover-content');
      const referenceSizeEl = trigger || ((_a = ev === null || ev === void 0 ? void 0 : ev.detail) === null || _a === void 0 ? void 0 : _a.ionShadowTarget) || (ev === null || ev === void 0 ? void 0 : ev.target);
 diff --git a/node_modules/@ionic/core/dist/esm/ion-popover.entry.js b/node_modules/@ionic/core/dist/esm/ion-popover.entry.js
-index 839e91c..abcd28f 100644
+index 8ca76cf..c5b990a 100644
 --- a/node_modules/@ionic/core/dist/esm/ion-popover.entry.js
 +++ b/node_modules/@ionic/core/dist/esm/ion-popover.entry.js
-@@ -764,8 +764,10 @@ const iosEnterAnimation = (baseEl, opts) => {
+@@ -765,8 +765,10 @@ const iosEnterAnimation = (baseEl, opts) => {
      const { event: ev, size, trigger, reference, side, align } = opts;
      const doc = baseEl.ownerDocument;
      const isRTL = doc.dir === 'rtl';
@@ -109,7 +109,7 @@ index 839e91c..abcd28f 100644
      const root = getElementRoot(baseEl);
      const contentEl = root.querySelector('.popover-content');
      const arrowEl = root.querySelector('.popover-arrow');
-@@ -885,8 +887,10 @@ const mdEnterAnimation = (baseEl, opts) => {
+@@ -886,8 +888,10 @@ const mdEnterAnimation = (baseEl, opts) => {
      const { event: ev, size, trigger, reference, side, align } = opts;
      const doc = baseEl.ownerDocument;
      const isRTL = doc.dir === 'rtl';
@@ -123,10 +123,10 @@ index 839e91c..abcd28f 100644
      const contentEl = root.querySelector('.popover-content');
      const referenceSizeEl = trigger || ((_a = ev === null || ev === void 0 ? void 0 : ev.detail) === null || _a === void 0 ? void 0 : _a.ionShadowTarget) || (ev === null || ev === void 0 ? void 0 : ev.target);
 diff --git a/node_modules/@ionic/core/hydrate/index.js b/node_modules/@ionic/core/hydrate/index.js
-index 7f898c7..a3a7669 100644
+index 5a50b98..884e2ce 100644
 --- a/node_modules/@ionic/core/hydrate/index.js
 +++ b/node_modules/@ionic/core/hydrate/index.js
-@@ -29254,8 +29254,10 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
+@@ -23702,8 +23702,10 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
      const { event: ev, size, trigger, reference, side, align } = opts;
      const doc = baseEl.ownerDocument;
      const isRTL = doc.dir === 'rtl';
@@ -139,7 +139,7 @@ index 7f898c7..a3a7669 100644
      const root = getElementRoot(baseEl);
      const contentEl = root.querySelector('.popover-content');
      const arrowEl = root.querySelector('.popover-arrow');
-@@ -29375,8 +29377,10 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
+@@ -23823,8 +23825,10 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
      const { event: ev, size, trigger, reference, side, align } = opts;
      const doc = baseEl.ownerDocument;
      const isRTL = doc.dir === 'rtl';
diff --git a/src/addons/calendar/components/calendar/calendar.scss b/src/addons/calendar/components/calendar/calendar.scss
index 1bdc04ba7..076847ba3 100644
--- a/src/addons/calendar/components/calendar/calendar.scss
+++ b/src/addons/calendar/components/calendar/calendar.scss
@@ -150,6 +150,6 @@
     }
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --addon-calendar-blank-day-background-color: var(--gray-900);
 }
diff --git a/src/addons/calendar/services/calendar-helper.ts b/src/addons/calendar/services/calendar-helper.ts
index be7426e56..7f32071b2 100644
--- a/src/addons/calendar/services/calendar-helper.ts
+++ b/src/addons/calendar/services/calendar-helper.ts
@@ -294,27 +294,6 @@ export class AddonCalendarHelperProvider {
         }
     }
 
-    /**
-     * Format reminders, adding calculated data.
-     *
-     * @param reminders Reminders.
-     * @param timestart Event timestart.
-     * @param siteId Site ID.
-     * @returns Formatted reminders.
-     * @deprecated since 4.1 Use AddonCalendarHelper.getEventReminders.
-     */
-    async formatReminders(
-        reminders: { eventid: number }[],
-        timestart: number,
-        siteId?: string,
-    ): Promise<AddonCalendarEventReminder[]> {
-        if (!reminders.length) {
-            return [];
-        }
-
-        return AddonCalendarHelper.getEventReminders(reminders[0].eventid, timestart, siteId);
-    }
-
     /**
      * Format reminders, adding calculated data.
      *
diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts
index 0b0633094..db0bc8437 100644
--- a/src/addons/calendar/services/calendar.ts
+++ b/src/addons/calendar/services/calendar.ts
@@ -45,10 +45,7 @@ import {
     CoreReminders,
     CoreRemindersPushNotificationData,
     CoreRemindersService,
-    CoreRemindersUnits,
-    CoreReminderValueAndUnit,
 } from '@features/reminders/services/reminders';
-import { CoreReminderDBRecord } from '@features/reminders/services/database/reminders';
 import { CoreEvents } from '@singletons/events';
 import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
 import { ADDON_CALENDAR_COMPONENT } from '../constants';
@@ -66,18 +63,6 @@ export enum AddonCalendarEventType {
     USER = 'user',
 }
 
-/**
- * Units to set a reminder.
- *
- * @deprecated since 4.1 Use CoreReminderUnits instead.
- */
-export enum AddonCalendarReminderUnits {
-    MINUTE = CoreConstants.SECONDS_MINUTE,
-    HOUR = CoreConstants.SECONDS_HOUR,
-    DAY = CoreConstants.SECONDS_DAY,
-    WEEK = CoreConstants.SECONDS_WEEK,
-}
-
 declare module '@singletons/events' {
 
     /**
@@ -178,17 +163,6 @@ export class AddonCalendarProvider {
         return !!site?.isVersionGreaterEqualThan('3.7.1');
     }
 
-    /**
-     * Given a number of seconds, convert it to a unit&value format compatible with reminders.
-     *
-     * @param seconds Number of seconds.
-     * @returns Value and unit.
-     * @deprecated since 4.1 Use CoreRemindersService.convertSecondsToValueAndUnit instead.
-     */
-    static convertSecondsToValueAndUnit(seconds: number): CoreReminderValueAndUnit {
-        return CoreRemindersService.convertSecondsToValueAndUnit(seconds);
-    }
-
     /**
      * Delete an event.
      *
@@ -592,17 +566,6 @@ export class AddonCalendarProvider {
         return CoreTimeUtils.userDate(time, 'core.strftimedayshort');
     }
 
-    /**
-     * Get the configured default notification time.
-     *
-     * @param siteId ID of the site. If not defined, use current site.
-     * @returns Promise resolved with the default time (in seconds).
-     * @deprecated since 4.1 Use CoreReminders.getDefaultNotificationTime instead.
-     */
-    async getDefaultNotificationTime(siteId?: string): Promise<number> {
-        return CoreReminders.getDefaultNotificationTime(siteId);
-    }
-
     /**
      * Get a calendar event. If the server request fails and data is not cached, try to get it from local DB.
      *
@@ -780,18 +743,6 @@ export class AddonCalendarProvider {
         return event.eventtype;
     }
 
-    /**
-     * Remove an event reminder and cancel the notification.
-     *
-     * @param id Reminder ID.
-     * @param siteId ID of the site the event belongs to. If not defined, use current site.
-     * @returns Promise resolved when the notification is updated.
-     * @deprecated since 4.1. Use CoreReminders.removeReminder instead.
-     */
-    async deleteEventReminder(id: number, siteId?: string): Promise<void> {
-        await CoreReminders.removeReminder(id, siteId);
-    }
-
     /**
      * Get calendar events for a certain day.
      *
@@ -876,21 +827,6 @@ export class AddonCalendarProvider {
                 (categoryId ? categoryId : '');
     }
 
-    /**
-     * Get a calendar reminders from local Db.
-     *
-     * @param eventId Event ID.
-     * @param siteId ID of the site the event belongs to. If not defined, use current site.
-     * @returns Promise resolved when the event data is retrieved.
-     * @deprecated since 4.1. Use CoreReminders.getReminders instead.
-     */
-    async getEventReminders(eventId: number, siteId?: string): Promise<CoreReminderDBRecord[]> {
-        return CoreReminders.getReminders({
-            instanceId: eventId,
-            component: ADDON_CALENDAR_COMPONENT,
-        }, siteId);
-    }
-
     /**
      * Get the events in a certain period. The period is calculated like this:
      *     start time: now + daysToStart
@@ -1089,19 +1025,6 @@ export class AddonCalendarProvider {
                 (categoryId ? categoryId : '');
     }
 
-    /**
-     * Given a value and a unit, return the translated label.
-     *
-     * @param value Value.
-     * @param unit Unit.
-     * @param addDefaultLabel Whether to add the "Default" text.
-     * @returns Translated label.
-     * @deprecated since 4.1 Use CoreReminders.getUnitValueLabel instead.
-     */
-    getUnitValueLabel(value: number, unit: CoreRemindersUnits, addDefaultLabel = false): string {
-        return CoreReminders.getUnitValueLabel(value, unit, addDefaultLabel);
-    }
-
     /**
      * Get upcoming calendar events.
      *
@@ -1378,16 +1301,6 @@ export class AddonCalendarProvider {
         return this.isCalendarDisabledInSite(site);
     }
 
-    /**
-     * Get the next events for all the sites and schedules their notifications.
-     *
-     * @returns Promise resolved when done.
-     * @deprecated since 4.1 Use AddonCalendar.updateAllSitesEventReminders.
-     */
-    async scheduleAllSitesEventsNotifications(): Promise<void> {
-        await AddonCalendar.updateAllSitesEventReminders();
-    }
-
     /**
      * Get the next events for all the sites and updates their reminders.
      */
@@ -1415,21 +1328,6 @@ export class AddonCalendarProvider {
         await this.getEventsList(undefined, undefined, undefined, siteId);
     }
 
-    /**
-     * Get the next events for all the sites and schedules their notifications.
-     *
-     * @returns Promise resolved when done.
-     * @deprecated since 4.1. No replacement for that function.
-     */
-    async scheduleEventsNotifications(
-        events: ({ id: number; timestart: number; timeduration: number; name: string})[],
-        siteId?: string,
-    ): Promise<void> {
-        siteId = siteId || CoreSites.getCurrentSiteId();
-
-        await AddonCalendar.updateEventsReminders(events, siteId);
-    }
-
     /**
      * Schedules the notifications for a list of events.
      * If an event notification time is 0, cancel its scheduled notification (if any).
@@ -1470,18 +1368,6 @@ export class AddonCalendarProvider {
         }));
     }
 
-    /**
-     * Set the default notification time.
-     *
-     * @param time New default time.
-     * @param siteId ID of the site. If not defined, use current site.
-     * @returns Promise resolved when stored.
-     * @deprecated since 4.1 Use CoreReminders.setDefaultNotificationTime.
-     */
-    async setDefaultNotificationTime(time: number, siteId?: string): Promise<void> {
-        await CoreReminders.setDefaultNotificationTime(time, siteId);
-    }
-
     /**
      * Store an event in local DB as it is.
      *
@@ -2203,13 +2089,6 @@ export type AddonCalendarUpdatedEventEvent = {
     sent?: boolean;
 };
 
-/**
- * Value and unit for reminders.
- *
- * @deprecated since 4.1, use CoreReminderValueAndUnit instead.
- */
-export type AddonCalendarValueAndUnit = CoreReminderValueAndUnit;
-
 /**
  * Options to pass to submit event.
  */
diff --git a/src/addons/messages/pages/discussion/discussion.html b/src/addons/messages/pages/discussion/discussion.html
index 0524b379a..6425cc125 100644
--- a/src/addons/messages/pages/discussion/discussion.html
+++ b/src/addons/messages/pages/discussion/discussion.html
@@ -77,7 +77,7 @@
                     {{ message.timecreated | coreFormatDate: "strftimedayshort" }}
                 </h3>
 
-                <ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom > 0 && message.id === unreadMessageFrom" color="light">
+                <ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom > 0 && message.id === unreadMessageFrom">
                     <ion-label>{{ 'addon.messages.newmessages' | translate }}</ion-label>
                     <ion-icon name="fas-arrow-down" aria-hidden="true" />
                 </ion-chip>
diff --git a/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_30.png b/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_30.png
index 80a903af6..c324279ef 100644
Binary files a/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_30.png and b/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_30.png differ
diff --git a/src/addons/mod/assign/components/submission/submission.scss b/src/addons/mod/assign/components/submission/submission.scss
index ba7e1b126..d2642ed7f 100644
--- a/src/addons/mod/assign/components/submission/submission.scss
+++ b/src/addons/mod/assign/components/submission/submission.scss
@@ -40,7 +40,7 @@
     }
 }
 
-:host-context(html.dark) ::ng-deep {
+:host-context(:root.dark) ::ng-deep {
     ion-item.submissioneditable p {
         color: var(--danger-tint);
     }
diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts
index 9e6011c32..420b69701 100644
--- a/src/addons/mod/assign/components/submission/submission.ts
+++ b/src/addons/mod/assign/components/submission/submission.ts
@@ -449,7 +449,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
                 this.feedback,
                 this.submitId,
             );
-        } catch (error) {
+        } catch {
             // Error ocurred, consider there are no changes.
             return false;
         }
diff --git a/src/addons/mod/assign/pages/submission-review/submission-review.html b/src/addons/mod/assign/pages/submission-review/submission-review.html
index fe477e715..dd7d7950a 100644
--- a/src/addons/mod/assign/pages/submission-review/submission-review.html
+++ b/src/addons/mod/assign/pages/submission-review/submission-review.html
@@ -13,7 +13,7 @@
     </ion-toolbar>
 
     <core-navbar-buttons slot="end">
-        <ion-button [hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()">
+        <ion-button [class.hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()">
             {{ 'core.done' | translate }}
         </ion-button>
     </core-navbar-buttons>
diff --git a/src/addons/mod/assign/tests/behat/marking_workflow.feature b/src/addons/mod/assign/tests/behat/marking_workflow.feature
index 5bb39e488..2e9b33534 100755
--- a/src/addons/mod/assign/tests/behat/marking_workflow.feature
+++ b/src/addons/mod/assign/tests/behat/marking_workflow.feature
@@ -100,6 +100,7 @@ Feature: Test marking workflow in assignment activity in app
     And I press "Grade" in the app
     When I set the field "Grade out of 100" to "60" in the app
     And I press "Done" in the app
+    And I wait loading to finish in the app
     And I press "Student1" in the app
     And I press "Grade" in the app
     Then I should find "60 / 100" within "Current grade in assignment" "ion-item" in the app
@@ -112,6 +113,7 @@ Feature: Test marking workflow in assignment activity in app
     And I press "Grade" in the app
     When I set the field "Grade out of 100" to "80" in the app
     And I press "Done" in the app
+    And I wait loading to finish in the app
     And I press "Student3" in the app
     And I press "Grade" in the app
     Then I should find "80" within "Current grade in gradebook" "ion-item" in the app
diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.scss b/src/addons/mod/bigbluebuttonbn/components/index/index.scss
index 1aac75966..b586d6845 100644
--- a/src/addons/mod/bigbluebuttonbn/components/index/index.scss
+++ b/src/addons/mod/bigbluebuttonbn/components/index/index.scss
@@ -24,7 +24,7 @@
     }
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --recording-details-background: var(--gray-800);
     --recording-details-border: var(--gray-500);
 }
diff --git a/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature b/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature
index 84e49873b..9ca94cf76 100755
--- a/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature
+++ b/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature
@@ -49,6 +49,8 @@ Feature: Test basic usage of BBB activity in app
     And I should be able to press "Join session" in the app
 
     When I press "Join session" in the app
+    # TODO: This step will make behat github actions work but we should find a better way to wait for the room to start.
+    And I wait "3" seconds
     And I wait for the BigBlueButton room to start
     And I switch back to the app
     Then I should find "The session is in progress." in the app
@@ -68,6 +70,8 @@ Feature: Test basic usage of BBB activity in app
     And I should be able to press "Join session" in the app
 
     When I press "Join session" in the app
+    # TODO: This step will make behat github actions work but we should find a better way to wait for the room to start.
+    And I wait "3" seconds
     And I wait for the BigBlueButton room to start
     And I switch back to the app
     Then I should find "The session is in progress." in the app
diff --git a/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png b/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png
index 1a1b3c1de..2f932c02b 100644
Binary files a/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png and b/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png differ
diff --git a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss
index a9021111d..e8045e634 100644
--- a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss
+++ b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.scss
@@ -35,7 +35,7 @@
     }
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     .addon-mod_h5pactivity-result-table-row.item:nth-child(even) {
         --background: var(--gray-900);
     }
diff --git a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html
index acdf9307a..6478c5c9f 100644
--- a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html
+++ b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html
@@ -31,7 +31,7 @@
                             <ion-input labelPlacement="stacked" name="password" type="password"
                                 placeholder="{{ 'core.login.password' | translate }}" core-auto-focus #passwordinput [clearOnEdit]="false"
                                 [label]="'addon.mod_lesson.enterpassword' | translate">
-                                <core-show-password slot="end" />
+                                <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" />
                             </ion-input>
                         </ion-item>
                         <ion-button expand="block" type="submit">
diff --git a/src/addons/mod/lesson/pages/player/player.scss b/src/addons/mod/lesson/pages/player/player.scss
index 2090c01ed..4afb34c08 100644
--- a/src/addons/mod/lesson/pages/player/player.scss
+++ b/src/addons/mod/lesson/pages/player/player.scss
@@ -2,7 +2,7 @@
    --background-odd: var(--light);
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
    --background-odd: var(--medium);
 }
 
diff --git a/src/addons/mod/quiz/accessrules/password/component/addon-mod-quiz-access-password.html b/src/addons/mod/quiz/accessrules/password/component/addon-mod-quiz-access-password.html
index b2d553869..4e7738556 100644
--- a/src/addons/mod/quiz/accessrules/password/component/addon-mod-quiz-access-password.html
+++ b/src/addons/mod/quiz/accessrules/password/component/addon-mod-quiz-access-password.html
@@ -8,6 +8,6 @@
     <ion-input id="addon-mod_quiz-accessrule-password-input" name="quizpassword" type="password"
         placeholder="{{ 'addon.mod_quiz.quizpassword' | translate }}" [formControlName]="'quizpassword'" [clearOnEdit]="false"
         [attr.aria-label]="'addon.mod_quiz.quizpassword' | translate">
-        <core-show-password slot="end" />
+        <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" />
     </ion-input>
 </ion-item>
diff --git a/src/addons/mod/quiz/pages/player/player.html b/src/addons/mod/quiz/pages/player/player.html
index b9c3574f1..e3de19c9b 100644
--- a/src/addons/mod/quiz/pages/player/player.html
+++ b/src/addons/mod/quiz/pages/player/player.html
@@ -11,7 +11,7 @@
         </ion-title>
 
         <ion-buttons slot="end">
-            <ion-button fill="clear" id="addon-mod_quiz-connection-error-button" [hidden]="!autoSaveError"
+            <ion-button fill="clear" id="addon-mod_quiz-connection-error-button" [class.hidden]="!autoSaveError"
                 (click)="showConnectionError($event)" [ariaLabel]="'addon.mod_quiz.connectionerror' | translate" aria-haspopup="dialog">
                 <ion-icon name="fas-circle-exclamation" slot="icon-only" aria-hidden="true" />
             </ion-button>
diff --git a/src/addons/mod/quiz/tests/behat/basic_usage.feature b/src/addons/mod/quiz/tests/behat/basic_usage.feature
index d02d3e1e2..0c6b90692 100755
--- a/src/addons/mod/quiz/tests/behat/basic_usage.feature
+++ b/src/addons/mod/quiz/tests/behat/basic_usage.feature
@@ -167,12 +167,16 @@ Feature: Attempt a quiz in app
     And I press "Three" in the app
     And I set the field "Answer" to "Berlin" in the app
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I set the field "Answer" to "testing" in the app
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I set the field "Answer" to "5" in the app
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I set the field "Answer" to "Testing an essay" in the app
     And I press "Next" "ion-button" in the app
+    And I wait loading to finish in the app
     And I press "quick" ".drag" in the app
     And I click on ".place1.drop" "css"
     And I press "fox" ".drag" in the app
@@ -180,26 +184,33 @@ Feature: Attempt a quiz in app
     And I press "lazy" ".drag" in the app
     And I click on ".place3.drop" "css"
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I press "True" in the app
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I set the field "frog" to "amphibian" in the app
     And I set the field "newt" to "insect" in the app
     And I set the field "cat" to "mammal" in the app
     And I press "Next" in the app
+    And I wait loading to finish in the app
     Then I should find "Text of the eighth question" in the app
 
     When I press "Next" in the app
+    And I wait loading to finish in the app
     And I set the field "Blank 1" to "cat" in the app
     And I set the field "Blank 2" to "mat" in the app
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I press "abyssal" ".drag" in the app
     And I click on ".place6.dropzone" "css"
     And I press "trench" ".drag" in the app
     And I click on ".place3.dropzone" "css"
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I press "Railway station" ".marker" in the app
     And I click on "img.dropbackground" "css"
     And I press "Submit" in the app
+    And I wait loading to finish in the app
     Then I should find "Answer saved" in the app
     And I should find "Incomplete answer" within "10" "ion-item" in the app
     But I should not find "Not yet answered" in the app
@@ -224,8 +235,10 @@ Feature: Attempt a quiz in app
 
     When I press "True" in the app
     And I press "Next" in the app
+    And I wait loading to finish in the app
     And I press "False" in the app
     And I press "Submit" in the app
+    And I wait loading to finish in the app
     And I press "Submit all and finish" in the app
     Then I should find "Once you submit" in the app
     But I should not find "Questions without a response" in the app
diff --git a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_40.png b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_42.png
similarity index 100%
rename from src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_40.png
rename to src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_42.png
diff --git a/src/addons/mod/survey/components/index/index.scss b/src/addons/mod/survey/components/index/index.scss
index 42e6f410c..9782bbfa1 100644
--- a/src/addons/mod/survey/components/index/index.scss
+++ b/src/addons/mod/survey/components/index/index.scss
@@ -25,7 +25,7 @@
     }
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --grid-background: var(--gray-900);
     --even-background: var(--medium);
 }
diff --git a/src/addons/mod/wiki/components/index/index.scss b/src/addons/mod/wiki/components/index/index.scss
index e2a7ffaac..7ae6942ed 100644
--- a/src/addons/mod/wiki/components/index/index.scss
+++ b/src/addons/mod/wiki/components/index/index.scss
@@ -56,7 +56,7 @@ $addon-mod-wiki-toc-level-padding:    12px !default;
     }
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --addon-mod-wiki-newentry-link-color: var(--danger-tint);
     --addon-mod-wiki-toc-background-color: var(--medium);
 }
diff --git a/src/addons/mod/workshop/pages/submission/submission.html b/src/addons/mod/workshop/pages/submission/submission.html
index 6c5a1afb1..dde29a697 100644
--- a/src/addons/mod/workshop/pages/submission/submission.html
+++ b/src/addons/mod/workshop/pages/submission/submission.html
@@ -10,8 +10,7 @@
             </h1>
         </ion-title>
         <ion-buttons slot="end" [hidden]="!loaded">
-            <ion-button *ngIf="assessmentId && access.assessingallowed" fill="clear" (click)="saveAssessment()"
-                [ariaLabel]="'core.save' | translate">
+            <ion-button *ngIf="assessmentId && access.assessingallowed" fill="clear" (click)="saveAssessment()">
                 {{ 'core.save' | translate }}
             </ion-button>
             <ion-button *ngIf="canAddFeedback" fill="clear" (click)="saveEvaluation()">
diff --git a/src/addons/notes/pages/list/list.html b/src/addons/notes/pages/list/list.html
index 058c56ec6..7cdafa23f 100644
--- a/src/addons/notes/pages/list/list.html
+++ b/src/addons/notes/pages/list/list.html
@@ -11,7 +11,8 @@
     </ion-toolbar>
 </ion-header>
 <core-navbar-buttons slot="end">
-    <ion-button [hidden]="!canDeleteNotes" slot="end" fill="clear" (click)="toggleDelete()" [ariaLabel]="'core.toggledelete' | translate">
+    <ion-button [class.hidden]="!canDeleteNotes" slot="end" fill="clear" (click)="toggleDelete()"
+        [ariaLabel]="'core.toggledelete' | translate">
         <ion-icon name="fas-pen" slot="icon-only" aria-hidden="true" />
     </ion-button>
     <core-context-menu>
diff --git a/src/addons/notifications/services/notifications.ts b/src/addons/notifications/services/notifications.ts
index c25c81fa1..49ed2a791 100644
--- a/src/addons/notifications/services/notifications.ts
+++ b/src/addons/notifications/services/notifications.ts
@@ -209,72 +209,6 @@ export class AddonNotificationsProvider {
         return ROOT_CACHE_KEY + 'list';
     }
 
-    /**
-     * Get some notifications.
-     *
-     * @param notifications Current list of loaded notifications. It's used to calculate the offset.
-     * @param options Other options.
-     * @returns Promise resolved with notifications and if can load more.
-     * @deprecated since 4.1. Use getNotificationsWithStatus instead.
-     */
-    async getNotifications(
-        notifications: AddonNotificationsNotificationMessageFormatted[],
-        options?: AddonNotificationsGetNotificationsOptions,
-    ): Promise<{notifications: AddonNotificationsNotificationMessageFormatted[]; canLoadMore: boolean}> {
-
-        notifications = notifications || [];
-        options = options || {};
-        options.limit = options.limit || AddonNotificationsProvider.LIST_LIMIT;
-        options.siteId = options.siteId || CoreSites.getCurrentSiteId();
-        let newNotifications: AddonNotificationsNotificationMessageFormatted[];
-
-        // Request 1 more notification so we can know if there are more notifications.
-        const originalLimit = options.limit;
-        options.limit = options.limit + 1;
-
-        const site = await CoreSites.getSite(options.siteId);
-
-        if (site.isVersionGreaterEqualThan('4.0')) {
-            // In 4.0 the app can request read and unread at the same time.
-            options.offset = notifications.length;
-            newNotifications = await this.getNotificationsWithStatus(
-                AddonNotificationsGetReadType.BOTH,
-                options,
-            );
-        } else {
-            // We need 2 calls, one for read and the other one for unread.
-            options.offset = notifications.reduce((total, current) => total + (current.read ? 0 : 1), 0);
-
-            const unread = await this.getNotificationsWithStatus(AddonNotificationsGetReadType.UNREAD, options);
-
-            newNotifications = unread;
-
-            if (unread.length < options.limit) {
-                // Limit not reached. Get read notifications until reach the limit.
-                const readOptions = {
-                    ...options,
-                    offset: notifications.length - options.offset,
-                    limit: options.limit - unread.length,
-                };
-
-                try {
-                    const read = await this.getNotificationsWithStatus(AddonNotificationsGetReadType.READ, readOptions);
-
-                    newNotifications = unread.concat(read);
-                } catch (error) {
-                    if (unread.length <= 0) {
-                        throw error;
-                    }
-                }
-            }
-        }
-
-        return {
-            notifications: newNotifications.slice(0, originalLimit),
-            canLoadMore: newNotifications.length > originalLimit,
-        };
-    }
-
     /**
      * Get notifications from site.
      *
diff --git a/src/core/classes/errors/siteerror.ts b/src/core/classes/errors/siteerror.ts
index 3eed531e6..9e1f2f569 100644
--- a/src/core/classes/errors/siteerror.ts
+++ b/src/core/classes/errors/siteerror.ts
@@ -31,7 +31,7 @@ export class CoreSiteError extends CoreError {
     }
 
     /**
-     * @deprecated This getter should not be called directly, but it's defined for backwards compatibility with many
+     * @deprecated since 4.4. This getter should not be called directly, but it's defined for backwards compatibility with many
      * parts of the code that type errors as any and use it. We cannot rename those because the errors could also be
      * CoreWSError instances which do have an "errorcode" property.
      *
diff --git a/src/core/classes/promised-value.ts b/src/core/classes/promised-value.ts
index 24f173224..d437e841b 100644
--- a/src/core/classes/promised-value.ts
+++ b/src/core/classes/promised-value.ts
@@ -55,14 +55,6 @@ export class CorePromisedValue<T = unknown> extends CorePromise<T> {
         this.rejectPromise = rejectPromise;
     }
 
-    /**
-     * @returns Promise.
-     * @deprecated since 4.1. The instance can be directly used as a promise.
-     */
-    get promise(): Promise<T> {
-        return this;
-    }
-
     get value(): T | null {
         return this.resolvedValue ?? null;
     }
diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts
index 2a8af678a..80e6c07e3 100644
--- a/src/core/classes/sites/site.ts
+++ b/src/core/classes/sites/site.ts
@@ -475,23 +475,6 @@ export class CoreSite extends CoreAuthenticatedSite {
         await this.openWithAutoLogin(false, url, options, alertMessage);
     }
 
-    /**
-     * Open a URL in browser using auto-login in the Moodle site if available and the URL belongs to the site.
-     *
-     * @param url The URL to open.
-     * @param alertMessage If defined, an alert will be shown before opening the browser.
-     * @param options Other options.
-     * @returns Promise resolved when done, rejected otherwise.
-     * @deprecated since 4.1. Use openInBrowserWithAutoLogin instead, now it always checks that URL belongs to same site.
-     */
-    async openInBrowserWithAutoLoginIfSameSite(
-        url: string,
-        alertMessage?: string,
-        options: CoreUtilsOpenInBrowserOptions = {},
-    ): Promise<void> {
-        return this.openInBrowserWithAutoLogin(url, alertMessage, options);
-    }
-
     /**
      * Open a URL in inappbrowser using auto-login in the Moodle site if available.
      *
@@ -506,23 +489,6 @@ export class CoreSite extends CoreAuthenticatedSite {
         return iabInstance;
     }
 
-    /**
-     * Open a URL in inappbrowser using auto-login in the Moodle site if available and the URL belongs to the site.
-     *
-     * @param url The URL to open.
-     * @param options Override default options passed to inappbrowser.
-     * @param alertMessage If defined, an alert will be shown before opening the inappbrowser.
-     * @returns Promise resolved when done.
-     * @deprecated since 4.1. Use openInAppWithAutoLogin instead, now it always checks that URL belongs to same site.
-     */
-    async openInAppWithAutoLoginIfSameSite(
-        url: string,
-        options?: InAppBrowserOptions,
-        alertMessage?: string,
-    ): Promise<InAppBrowserObject> {
-        return this.openInAppWithAutoLogin(url, options, alertMessage);
-    }
-
     /**
      * Open a URL in browser or InAppBrowser using auto-login in the Moodle site if available.
      *
@@ -575,25 +541,6 @@ export class CoreSite extends CoreAuthenticatedSite {
         }
     }
 
-    /**
-     * Open a URL in browser or InAppBrowser using auto-login in the Moodle site if available and the URL belongs to the site.
-     *
-     * @param inApp True to open it in InAppBrowser, false to open in browser.
-     * @param url The URL to open.
-     * @param options Override default options passed to inappbrowser.
-     * @param alertMessage If defined, an alert will be shown before opening the browser/inappbrowser.
-     * @returns Promise resolved when done. Resolve param is returned only if inApp=true.
-     * @deprecated since 4.1. Use openWithAutoLogin instead, now it always checks that URL belongs to same site.
-     */
-    async openWithAutoLoginIfSameSite(
-        inApp: boolean,
-        url: string,
-        options: InAppBrowserOptions & CoreUtilsOpenInBrowserOptions = {},
-        alertMessage?: string,
-    ): Promise<InAppBrowserObject | void> {
-        return this.openWithAutoLogin(inApp, url, options, alertMessage);
-    }
-
     /**
      * Get the config of this site.
      * It is recommended to use getStoredConfig instead since it's faster and doesn't use network.
diff --git a/src/core/components/combobox/combobox.scss b/src/core/components/combobox/combobox.scss
index a42acf707..dec9eec50 100644
--- a/src/core/components/combobox/combobox.scss
+++ b/src/core/components/combobox/combobox.scss
@@ -146,7 +146,7 @@
         }
 
         .select-icon {
-            color: var(--ion-color-step-500, gray);
+            color: var(--ion-text-color-step-500, gray);
         }
     }
 
diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts
index 8055d6e70..23b32cf00 100644
--- a/src/core/components/components.module.ts
+++ b/src/core/components/components.module.ts
@@ -97,7 +97,7 @@ import { CoreSitesListComponent } from './sites-list/sites-list';
         CoreProgressBarComponent,
         CoreRecaptchaComponent,
         CoreSendMessageFormComponent,
-        CoreShowPasswordComponent,
+        CoreShowPasswordComponent, // eslint-disable-line deprecation/deprecation
         CoreSitePickerComponent,
         CoreSplitViewComponent,
         // eslint-disable-next-line deprecation/deprecation
@@ -153,7 +153,7 @@ import { CoreSitesListComponent } from './sites-list/sites-list';
         CoreProgressBarComponent,
         CoreRecaptchaComponent,
         CoreSendMessageFormComponent,
-        CoreShowPasswordComponent,
+        CoreShowPasswordComponent, // eslint-disable-line deprecation/deprecation
         CoreSitePickerComponent,
         CoreSplitViewComponent,
         // eslint-disable-next-line deprecation/deprecation
diff --git a/src/core/components/context-menu/core-context-menu.html b/src/core/components/context-menu/core-context-menu.html
index dee543e37..175bcd677 100644
--- a/src/core/components/context-menu/core-context-menu.html
+++ b/src/core/components/context-menu/core-context-menu.html
@@ -1,4 +1,4 @@
-<ion-button [hidden]="hideMenu" fill="clear" [ariaLabel]="ariaLabel" (click)="showContextMenu($event)" aria-haspopup="true"
+<ion-button [class.hidden]="hideMenu" fill="clear" [ariaLabel]="ariaLabel" (click)="showContextMenu($event)" aria-haspopup="true"
     [attr.aria-controls]="uniqueId">
     <ion-icon [name]="icon" slot="icon-only" aria-hidden="true" />
 </ion-button>
diff --git a/src/core/components/empty-box/empty-box.scss b/src/core/components/empty-box/empty-box.scss
index b79b0305d..e87befd6e 100644
--- a/src/core/components/empty-box/empty-box.scss
+++ b/src/core/components/empty-box/empty-box.scss
@@ -38,7 +38,7 @@
     }
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     &.dimmed {
         --text-color: var(--gray-300);
     }
diff --git a/src/core/components/mod-icon/mod-icon.scss b/src/core/components/mod-icon/mod-icon.scss
index fb23300c9..24bfad6fe 100644
--- a/src/core/components/mod-icon/mod-icon.scss
+++ b/src/core/components/mod-icon/mod-icon.scss
@@ -78,7 +78,7 @@
     --margin-end: 12px;
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     &.version_40:not(.colorize),
     &.version_current {
         background-color: var(--white);
diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts
index 349377eec..631732347 100644
--- a/src/core/components/navbar-buttons/navbar-buttons.ts
+++ b/src/core/components/navbar-buttons/navbar-buttons.ts
@@ -40,12 +40,12 @@ const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
  *
  * You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
  *
- * IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead.
+ * IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [class.hidden] instead.
  *
  * Example usage:
  *
  * <core-navbar-buttons slot="end">
- *     <ion-button [hidden]="!buttonShown" [attr.aria-label]="Do something" (click)="action()">
+ *     <ion-button [class.hidden]="!buttonShown" [ariaLabel]="Do something" (click)="action()">
  *         <ion-icon name="funnel" slot="icon-only" aria-hidden="true"></ion-icon>
  *     </ion-button>
  * </core-navbar-buttons>
diff --git a/src/core/components/password-modal/password-modal.html b/src/core/components/password-modal/password-modal.html
index 8b1b77ef6..a8c99151d 100644
--- a/src/core/components/password-modal/password-modal.html
+++ b/src/core/components/password-modal/password-modal.html
@@ -17,7 +17,7 @@
                 <ion-input [ariaLabel]="placeholder | translate" class="ion-text-wrap core-ioninput-password" name="password"
                     type="password" placeholder="{{ placeholder | translate }}" [(ngModel)]="password" core-auto-focus
                     [clearOnEdit]="false">
-                    <core-show-password slot="end" />
+                    <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" />
                 </ion-input>
             </ion-item>
             <ion-item *ngIf="error" class="ion-text-wrap ion-padding-top text-danger">
diff --git a/src/core/components/show-password/core-show-password.html b/src/core/components/show-password/core-show-password.html
index 21642a1ce..715bd5bf0 100644
--- a/src/core/components/show-password/core-show-password.html
+++ b/src/core/components/show-password/core-show-password.html
@@ -1,5 +1,2 @@
 <ng-content />
-<ion-button fill="clear" [ariaLabel]="(shown ? 'core.hide' : 'core.show') | translate" core-suppress-events (onClick)="toggle($event)"
-    (mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
-    <ion-icon [name]="shown ? 'fas-eye-slash' : 'fas-eye'" slot="icon-only" aria-hidden="true" />
-</ion-button>
+<ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" *ngIf="!this.ionInput" />
diff --git a/src/core/components/show-password/show-password.scss b/src/core/components/show-password/show-password.scss
deleted file mode 100644
index ec8244fb6..000000000
--- a/src/core/components/show-password/show-password.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-@use "theme/globals" as *;
-
-:host {
-    display: contents;
-
-    // Only applies to deprecated way (surrounding).
-    ::ng-deep ion-input + ion-button {
-        background: transparent;
-        padding: 0 var(--inner-padding-end) 0 4px;
-        margin-top: 0;
-        margin-bottom: 0;
-        position: absolute;
-        @include safe-area-position(null, 0px, null, null);
-        top: 0;
-    }
-
-    // Only applies to deprecated way (surrounding).
-    ::ng-deep ion-input {
-        --padding-end: 56px !important;
-    }
-
-    ::ng-deep ion-input.input-label-placement-stacked + ion-button {
-        top: 14px;
-    }
-
-}
-
-ion-button {
-    z-index: 5;
-    pointer-events: visible;
-}
diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts
index 506f50e6d..88bdcc81f 100644
--- a/src/core/components/show-password/show-password.ts
+++ b/src/core/components/show-password/show-password.ts
@@ -12,173 +12,80 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, OnInit, AfterViewInit, Input, ElementRef, ContentChild } from '@angular/core';
+import { Component, AfterViewInit, Input, ContentChild, ViewEncapsulation } from '@angular/core';
 import { IonInput } from '@ionic/angular';
-
-import { CorePlatform } from '@services/platform';
 import { CoreDomUtils } from '@services/utils/dom';
+
 import { CoreUtils } from '@services/utils/utils';
 import { CoreLogger } from '@singletons/logger';
 
 /**
  * This component allows to show/hide a password.
- * It's meant to be used with ion-input.
- * It's recommended to use it as a slot of the input.
+ * It's meant to be used with ion-input as a slot of the input.
  *
  * @description
  *
  * There are 2 ways to use ths component:
  * - Slot it to start or end on the ion-input element.
- * - Surround the ion-input with the password with this component. This is deprecated.
- *
- * In order to help finding the input you can specify the name of the input or the ion-input element.
- *
+ * - Surround the ion-input with the password with this component. Not recommended.
  *
  * Example of new usage:
  *
- * <ion-input type="password" name="password">
+ * <ion-input type="password">
  *     <core-show-password slot="end" />
  * </ion-input>
  *
- * Example deprecated usage:
+ * Example surrounding usage:
  *
  * <core-show-password>
- *     <ion-input type="password" name="password"></ion-input>
+ *     <ion-input type="password" />
  * </core-show-password>
+ *
+ * @deprecated since 4.5. Use <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" /> instead.
  */
 @Component({
     selector: 'core-show-password',
     templateUrl: 'core-show-password.html',
-    styleUrls: ['show-password.scss'],
+    styles: 'core-show-password { display: contents; }',
+    encapsulation: ViewEncapsulation.None,
 })
-export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
-
-    @Input() initialShown?: boolean | string; // Whether the password should be shown at start.
-
-    @Input() name = ''; // Deprecated. Not used anymore.
-    @ContentChild(IonInput) ionInput?: IonInput | HTMLIonInputElement; // Deprecated. Use slot instead.
-
-    protected input?: HTMLInputElement;
-    protected hostElement: HTMLElement;
-    protected logger: CoreLogger;
-
-    constructor(element: ElementRef) {
-        this.hostElement = element.nativeElement;
-        this.logger = CoreLogger.getInstance('CoreShowPasswordComponent');
-    }
-
-    get shown(): boolean {
-        return this.input?.type === 'text';
-    }
-
-    set shown(shown: boolean) {
-        if (!this.input) {
-            return;
-        }
-
-        this.input.type = shown ? 'text' : 'password';
-    }
+export class CoreShowPasswordComponent implements AfterViewInit {
 
     /**
-     * @inheritdoc
+     * @deprecated since 4.5. Not used anymore.
      */
-    ngOnInit(): void {
-        this.shown = CoreUtils.isTrueOrOne(this.initialShown);
-    }
+    @Input() initialShown = '';
+
+    /**
+     * @deprecated since 4.4. Not used anymore.
+     */
+    @Input() name = '';
+
+    /**
+     * @deprecated since 4.4. Use slotted solution instead.
+     */
+    @ContentChild(IonInput) ionInput?: IonInput | HTMLIonInputElement;
 
     /**
      * @inheritdoc
      */
     async ngAfterViewInit(): Promise<void> {
-        await this.setInputElement();
-
-        if (!this.input) {
-            return;
-        }
-
-        // By default, don't autocapitalize and autocorrect.
-        if (!this.input.getAttribute('autocorrect')) {
-            this.input.setAttribute('autocorrect', 'off');
-        }
-        if (!this.input.getAttribute('autocapitalize')) {
-            this.input.setAttribute('autocapitalize', 'none');
-        }
-    }
-
-    /**
-     * Set the input element to affect.
-     */
-    protected async setInputElement(): Promise<void> {
-        if (!this.ionInput) {
-            this.ionInput = this.hostElement.closest('ion-input') ?? undefined;
-
-            this.hostElement.setAttribute('slot', 'end');
-        } else {
-            // It's outside ion-input, warn devs.
-            this.logger.warn('Deprecated CoreShowPasswordComponent usage, it\'s not needed to surround ion-input anymore.');
-        }
+        CoreLogger.getInstance('CoreShowPasswordComponent')
+            .warn('Deprecated component, use <ion-input-password-toggle /> instead.');
 
+        // eslint-disable-next-line deprecation/deprecation
         if (!this.ionInput) {
             return;
         }
 
-        try {
-            this.input = await this.ionInput.getInputElement();
-        } catch {
-            // This should never fail, but it does in some testing environment because Ionic elements are not
-            // rendered properly. So in case this fails it will try to find through the name and ignore the error.
-            const name = this.ionInput.name;
-            if (!name) {
-                return;
-            }
-            this.input = this.hostElement.querySelector<HTMLInputElement>('input[name="' + name + '"]') ?? undefined;
-        }
-    }
-
-    /**
-     * Toggle show/hide password.
-     *
-     * @param event The mouse event.
-     */
-    toggle(event: Event): void {
-        if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
+        // eslint-disable-next-line deprecation/deprecation
+        const input = await CoreUtils.ignoreErrors(this.ionInput.getInputElement());
+        if (!input) {
             return;
         }
 
-        event.preventDefault();
-        event.stopPropagation();
-
-        const isFocused = document.activeElement === this.input;
-        this.shown = !this.shown;
-
-        // In Android, the keyboard is closed when the input type changes. Focus it again.
-        if (this.input && isFocused && CorePlatform.isAndroid()) {
-            CoreDomUtils.focusElement(this.input);
-        }
-    }
-
-    /**
-     * Do not loose focus.
-     *
-     * @param event The mouse event.
-     */
-    doNotBlur(event: Event): void {
-        if (event.type === 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
-            return;
-        }
-
-        event.preventDefault();
-        event.stopPropagation();
-    }
-
-    /**
-     * Checks if Space or Enter have been pressed.
-     *
-     * @param event Keyboard Event.
-     * @returns Wether space or enter have been pressed.
-     */
-    protected isValidKeyboardKey(event: KeyboardEvent): boolean {
-        return event.key === ' ' || event.key === 'Enter';
+        const toggle = CoreDomUtils.convertToElement('<ion-input-password-toggle slot="end" />');
+        input.parentElement?.appendChild(toggle.children[0]);
     }
 
 }
diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts
index 63849d3d0..b253c6149 100644
--- a/src/core/features/compile/services/compile.ts
+++ b/src/core/features/compile/services/compile.ts
@@ -77,7 +77,6 @@ import { Md5 } from 'ts-md5/dist/md5';
 // Import core classes that can be useful for site plugins.
 import { CoreSyncBaseProvider } from '@classes/base-sync';
 import { CoreArray } from '@singletons/array';
-import { CoreComponentsRegistry } from '@singletons/components-registry';
 import { CoreDirectivesRegistry } from '@singletons/directives-registry';
 import { CoreDom } from '@singletons/dom';
 import { CoreForms } from '@singletons/form';
@@ -295,12 +294,13 @@ export class CoreCompileProvider {
         instance['CoreLoggerProvider'] = CoreLogger;
         instance['moment'] = moment;
         instance['Md5'] = Md5;
-        instance['Network'] = CoreNetwork.instance; // @deprecated since 4.1, plugins should use CoreNetwork instead.
-        instance['Platform'] = CorePlatform.instance; // @deprecated since 4.1, plugins should use CorePlatform instead.
+        /**
+         * @deprecated since 4.1, plugins should use CoreNetwork instead.
+         * Keeping this a bit more to avoid plugins breaking.
+         */
+        instance['Network'] = CoreNetwork.instance;
         instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider;
         instance['CoreArray'] = CoreArray;
-        // eslint-disable-next-line deprecation/deprecation
-        instance['CoreComponentsRegistry'] = CoreComponentsRegistry;
         instance['CoreDirectivesRegistry'] = CoreDirectivesRegistry;
         instance['CoreNetwork'] = CoreNetwork.instance;
         instance['CorePlatform'] = CorePlatform.instance;
diff --git a/src/core/features/course/components/module-description/module-description.ts b/src/core/features/course/components/module-description/module-description.ts
index d0bb74b58..738e89f9d 100644
--- a/src/core/features/course/components/module-description/module-description.ts
+++ b/src/core/features/course/components/module-description/module-description.ts
@@ -31,7 +31,8 @@ import { Component, HostBinding, Input } from '@angular/core';
  *
  * <core-course-module-description [description]="myDescription"></core-course-module-description>
  *
- * @deprecated since 4.0 use core-course-module-info
+ * @deprecated since 4.0 use core-course-module-info instead.
+ * Keeping this a bit more to avoid plugins breaking.
  */
 @Component({
     selector: 'core-course-module-description',
diff --git a/src/core/features/course/components/module-navigation/module-navigation.scss b/src/core/features/course/components/module-navigation/module-navigation.scss
index c43abc36f..71e9200c8 100644
--- a/src/core/features/course/components/module-navigation/module-navigation.scss
+++ b/src/core/features/course/components/module-navigation/module-navigation.scss
@@ -50,6 +50,6 @@
     height: 0 !important;
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --button-color: var(--gray-100);
 }
diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss
index be33f3f6e..19f40ee3c 100644
--- a/src/core/features/course/components/module/module.scss
+++ b/src/core/features/course/components/module/module.scss
@@ -198,7 +198,7 @@
 }
 
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     .activity-description-availabilityinfo {
         .core-module-availabilityinfo {
             background: var(--gray-800);
diff --git a/src/core/features/courses/components/components.module.ts b/src/core/features/courses/components/components.module.ts
index 591dc709d..a4444767f 100644
--- a/src/core/features/courses/components/components.module.ts
+++ b/src/core/features/courses/components/components.module.ts
@@ -16,14 +16,11 @@ import { NgModule } from '@angular/core';
 
 import { CoreSharedModule } from '@/core/shared.module';
 import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item';
-import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress';
 import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu';
 
 @NgModule({
     declarations: [
         CoreCoursesCourseListItemComponent,
-        // eslint-disable-next-line deprecation/deprecation
-        CoreCoursesCourseProgressComponent,
         CoreCoursesCourseOptionsMenuComponent,
     ],
     imports: [
@@ -31,8 +28,6 @@ import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/cou
     ],
     exports: [
         CoreCoursesCourseListItemComponent,
-        // eslint-disable-next-line deprecation/deprecation
-        CoreCoursesCourseProgressComponent,
         CoreCoursesCourseOptionsMenuComponent,
     ],
 })
diff --git a/src/core/features/courses/components/course-list-item/course-list-item.scss b/src/core/features/courses/components/course-list-item/course-list-item.scss
index c11f469ad..0f8ab669e 100644
--- a/src/core/features/courses/components/course-list-item/course-list-item.scss
+++ b/src/core/features/courses/components/course-list-item/course-list-item.scss
@@ -29,7 +29,7 @@
 }
 
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --button-background: rgb(0 0 0 / 30%);
 }
 
diff --git a/src/core/features/courses/components/course-progress/core-courses-course-progress.html b/src/core/features/courses/components/course-progress/core-courses-course-progress.html
deleted file mode 100644
index 94f024f2c..000000000
--- a/src/core/features/courses/components/course-progress/core-courses-course-progress.html
+++ /dev/null
@@ -1,2 +0,0 @@
-<core-courses-course-list-item [course]="course" class="core-course-progress" [showDownload]="showDownload"
-    [layout]="showAll ? 'card' : 'summarycard'" />
diff --git a/src/core/features/courses/components/course-progress/course-progress.ts b/src/core/features/courses/components/course-progress/course-progress.ts
deleted file mode 100644
index 463456237..000000000
--- a/src/core/features/courses/components/course-progress/course-progress.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// (C) Copyright 2015 Moodle Pty Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import { Component, HostBinding, Input } from '@angular/core';
-import { CoreCourseListItem } from '@features/courses/services/courses';
-
-/**
- * This component is meant to display a course for a list of courses with progress.
- *
- * Example usage:
- *
- * <core-courses-course-progress [course]="course">
- * </core-courses-course-progress>
- *
- * @deprecated since 4.0. Use core-courses-course-list-item instead.
- */
-@Component({
-    selector: 'core-courses-course-progress',
-    templateUrl: 'core-courses-course-progress.html',
-})
-export class CoreCoursesCourseProgressComponent {
-
-    @Input() course!: CoreCourseListItem; // The course to render.
-    @Input() showAll = false; // If true, will show all actions, options, star and progress.
-    @Input() showDownload = true; // If true, will show download button. Only works if the options menu is not shown.
-
-    @HostBinding('class.deprecated') get isDeprecated(): boolean {
-        return true;
-    }
-
-}
diff --git a/src/core/features/courses/pages/my/my.html b/src/core/features/courses/pages/my/my.html
index de4cf18de..933afcc04 100644
--- a/src/core/features/courses/pages/my/my.html
+++ b/src/core/features/courses/pages/my/my.html
@@ -27,7 +27,7 @@
                 <!-- Download all courses. -->
                 <div *ngIf="downloadCoursesEnabled && myOverviewBlock && myOverviewBlock.filteredCourses.length > 0"
                     class="core-button-spinner">
-                    <ion-button *ngIf="!myOverviewBlock.prefetchCoursesData.loading" fill="clear"
+                    <ion-button *ngIf="!myOverviewBlock.prefetchCoursesData.loading" fill="clear" size="default"
                         (click)="myOverviewBlock.prefetchCourses()"
                         [attr.aria-label]="myOverviewBlock.prefetchCoursesData.statusTranslatable | translate">
                         <ion-icon [name]="myOverviewBlock.prefetchCoursesData.icon" slot="icon-only" aria-hidden="true" />
diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss b/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss
index adc88eec5..5930d15d2 100644
--- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss
+++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss
@@ -6,7 +6,7 @@
     --background: var(--rte-editor-background);
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --color: var(--white);
     --button-color: var(--gray-200);
     --button-active-color: var(--gray-500);
diff --git a/src/core/features/grades/pages/course/course.scss b/src/core/features/grades/pages/course/course.scss
index a6bc87e94..880c195d9 100644
--- a/src/core/features/grades/pages/course/course.scss
+++ b/src/core/features/grades/pages/course/course.scss
@@ -5,7 +5,7 @@
     --core-table-border-color: var(--stroke);
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --icon-color: var(--gray-200);
 }
 
diff --git a/src/core/features/login/login.scss b/src/core/features/login/login.scss
index cd22bfad5..e02eaea05 100644
--- a/src/core/features/login/login.scss
+++ b/src/core/features/login/login.scss
@@ -166,7 +166,7 @@
 
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     @if ($core-login-button-outline-dark) {
         form ion-button {
             --background: white;
diff --git a/src/core/features/login/pages/credentials/credentials.html b/src/core/features/login/pages/credentials/credentials.html
index 3e7cf76f3..caacc02d9 100644
--- a/src/core/features/login/pages/credentials/credentials.html
+++ b/src/core/features/login/pages/credentials/credentials.html
@@ -53,7 +53,7 @@
                         <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
                             formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go"
                             required="true" [attr.aria-label]="'core.login.password' | translate ">
-                            <core-show-password slot="end" />
+                            <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" />
                         </ion-input>
                     </ion-item>
                     <ion-button expand="block" type="submit" [disabled]="!credForm.valid"
diff --git a/src/core/features/login/pages/email-signup/email-signup.html b/src/core/features/login/pages/email-signup/email-signup.html
index 1a8370834..51c27f859 100644
--- a/src/core/features/login/pages/email-signup/email-signup.html
+++ b/src/core/features/login/pages/email-signup/email-signup.html
@@ -103,7 +103,7 @@
                             placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
                             autocomplete="new-password" required="true">
                             <div slot="label" [core-mark-required]="true">{{ 'core.login.password' | translate }}</div>
-                            <core-show-password slot="end" />
+                            <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" />
                         </ion-input>
                         <p *ngIf="settings.passwordpolicy" class="core-input-footnote">
                             {{settings.passwordpolicy}}
diff --git a/src/core/features/login/pages/reconnect/reconnect.html b/src/core/features/login/pages/reconnect/reconnect.html
index 354a1a9b3..3c529966e 100644
--- a/src/core/features/login/pages/reconnect/reconnect.html
+++ b/src/core/features/login/pages/reconnect/reconnect.html
@@ -65,7 +65,7 @@
                             placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
                             autocomplete="current-password" enterkeyhint="go" required="true"
                             [attr.aria-label]="'core.login.password' | translate">
-                            <core-show-password slot="end" />
+                            <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" />
                         </ion-input>
                     </ion-item>
                     <ion-button type="submit" expand="block" [disabled]="!credForm.valid"
diff --git a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png
index 7e3e37007..5fa063d00 100644
Binary files a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png and b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png differ
diff --git a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png
index 1d2c22c52..dc3db50c8 100644
Binary files a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png and b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png differ
diff --git a/src/core/features/search/components/global-search-filters/global-search-filters.scss b/src/core/features/search/components/global-search-filters/global-search-filters.scss
index e7678a15c..d40b02857 100644
--- a/src/core/features/search/components/global-search-filters/global-search-filters.scss
+++ b/src/core/features/search/components/global-search-filters/global-search-filters.scss
@@ -11,6 +11,6 @@
     }
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --heading-text-color: var(--gray-400);
 }
diff --git a/src/core/features/search/components/global-search-result/global-search-result.scss b/src/core/features/search/components/global-search-result/global-search-result.scss
index 6b1464abe..7289a9ae4 100644
--- a/src/core/features/search/components/global-search-result/global-search-result.scss
+++ b/src/core/features/search/components/global-search-result/global-search-result.scss
@@ -91,7 +91,7 @@
 
 }
 
-:host-context(html.dark) ion-item {
+:host-context(:root.dark) ion-item {
     --core-global-search-result-content-color: var(--gray-400);
     --core-global-search-result-context-color: var(--gray-500);
 }
diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts
index 483e28c7c..47444868b 100644
--- a/src/core/features/settings/services/settings-helper.ts
+++ b/src/core/features/settings/services/settings-helper.ts
@@ -437,7 +437,7 @@ export class CoreSettingsHelperProvider {
         const isDark = CoreDomUtils.hasModeClass('dark');
 
         if (isDark !== enable) {
-            CoreDomUtils.toggleModeClass('dark', enable, { includeLegacy: true });
+            CoreDomUtils.toggleModeClass('dark', enable);
             this.darkModeObservable.next(enable);
 
             CoreApp.setSystemUIColors();
diff --git a/src/core/features/usertours/components/user-tour/user-tour.scss b/src/core/features/usertours/components/user-tour/user-tour.scss
index 175b55d90..91c276a7e 100644
--- a/src/core/features/usertours/components/user-tour/user-tour.scss
+++ b/src/core/features/usertours/components/user-tour/user-tour.scss
@@ -61,6 +61,6 @@
 
 }
 
-:host-context(html.dark) {
+:host-context(:root.dark) {
     --popover-background: var(--gray-700);
 }
diff --git a/src/core/services/app.ts b/src/core/services/app.ts
index e980222b2..88031995f 100644
--- a/src/core/services/app.ts
+++ b/src/core/services/app.ts
@@ -30,9 +30,9 @@ import { CoreDatabaseTable } from '@classes/database/database-table';
 import { CorePromisedValue } from '@classes/promised-value';
 import { Subscription } from 'rxjs';
 import { CorePlatform } from '@services/platform';
-import { CoreNetwork, CoreNetworkConnection } from '@services/network';
 import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
 import { CoreKeyboard } from '@singletons/keyboard';
+import { CoreNetwork } from './network';
 
 /**
  * Factory to provide some global functionalities, like access to the global app database.
@@ -210,36 +210,6 @@ export class CoreAppProvider {
         return storesConfig.default;
     }
 
-    /**
-     * Get platform major version number.
-     *
-     * @returns The platform major number.
-     * @deprecated since 4.1.1. Use CorePlatform.getPlatformMajorVersion instead.
-     */
-    getPlatformMajorVersion(): number {
-        return CorePlatform.getPlatformMajorVersion();
-    }
-
-    /**
-     * Checks if the app is running in an Android mobile or tablet device.
-     *
-     * @returns Whether the app is running in an Android mobile or tablet device.
-     * @deprecated since 4.1.1. Use CorePlatform.isAndroid instead.
-     */
-    isAndroid(): boolean {
-        return CorePlatform.isAndroid();
-    }
-
-    /**
-     * Checks if the app is running in an iOS mobile or tablet device.
-     *
-     * @returns Whether the app is running in an iOS mobile or tablet device.
-     * @deprecated since 4.1.1. Use CorePlatform.isIOS instead.
-     */
-    isIOS(): boolean {
-        return CorePlatform.isIOS();
-    }
-
     /**
      * Check if the keyboard is closing.
      *
@@ -270,16 +240,6 @@ export class CoreAppProvider {
         return CoreKeyboard.isKeyboardVisible();
     }
 
-    /**
-     * Checks if the app is running in a mobile or tablet device (Cordova).
-     *
-     * @returns Whether the app is running in a mobile or tablet device.
-     * @deprecated since 4.1. Use CorePlatform instead.
-     */
-    isMobile(): boolean {
-        return CorePlatform.isMobile();
-    }
-
     /**
      * Checks if the current window is wider than a mobile.
      *
@@ -294,31 +254,12 @@ export class CoreAppProvider {
      *
      * @returns Whether the app is online.
      * @deprecated since 4.1. Use CoreNetwork instead.
+     * Keeping this a bit more to avoid plugins breaking.
      */
     isOnline(): boolean {
         return CoreNetwork.isOnline();
     }
 
-    /**
-     * Check if device uses a limited connection.
-     *
-     * @returns Whether the device uses a limited connection.
-     * @deprecated since 4.1. Use CoreNetwork instead.
-     */
-    isNetworkAccessLimited(): boolean {
-        return CoreNetwork.isNetworkAccessLimited();
-    }
-
-    /**
-     * Check if device uses a wifi connection.
-     *
-     * @returns Whether the device uses a wifi connection.
-     * @deprecated since 4.1. Use CoreNetwork instead.
-     */
-    isWifi(): boolean {
-        return CoreNetwork.isWifi();
-    }
-
     /**
      * Open the keyboard.
      *
@@ -561,16 +502,6 @@ export class CoreAppProvider {
         StatusBar.backgroundColorByHexString(color);
     }
 
-    /**
-     * Set value of forceOffline flag. If true, the app will think the device is offline.
-     *
-     * @param value Value to set.
-     * @deprecated since 4.1. Use CoreNetwork.setForceConnectionMode instead.
-     */
-    setForceOffline(value: boolean): void {
-        CoreNetwork.setForceConnectionMode(value ? CoreNetworkConnection.NONE : CoreNetworkConnection.WIFI);
-    }
-
     /**
      * Get the installed version for the given schema.
      *
diff --git a/src/core/services/file.ts b/src/core/services/file.ts
index cf2f8f550..aa369f185 100644
--- a/src/core/services/file.ts
+++ b/src/core/services/file.ts
@@ -1015,17 +1015,6 @@ export class CoreFileProvider {
         return promise.then((entry) => this.getMetadata(entry));
     }
 
-    /**
-     * Remove the starting slash of a path if it's there. E.g. '/sites/filepool' -> 'sites/filepool'.
-     *
-     * @param path Path.
-     * @returns Path without a slash in the first position.
-     * @deprecated since 4.1. Use CoreText.removeStartingSlash instead.
-     */
-    removeStartingSlash(path: string): string {
-        return CoreText.removeStartingSlash(path);
-    }
-
     /**
      * Convenience function to copy or move an external file.
      *
diff --git a/src/core/services/local-notifications.ts b/src/core/services/local-notifications.ts
index 6ce207f53..f297b21ce 100644
--- a/src/core/services/local-notifications.ts
+++ b/src/core/services/local-notifications.ts
@@ -429,16 +429,6 @@ export class CoreLocalNotificationsProvider {
         }
     }
 
-    /**
-     * Returns whether local notifications are available.
-     *
-     * @returns Whether local notifications are available.
-     * @deprecated since 4.1. It will always return true.
-     */
-    isAvailable(): boolean {
-        return true;
-    }
-
     /**
      * Returns whether local notifications plugin is available.
      *
diff --git a/src/core/services/network.ts b/src/core/services/network.ts
index d7206e0b6..3c8cc18f9 100644
--- a/src/core/services/network.ts
+++ b/src/core/services/network.ts
@@ -111,22 +111,22 @@ export class CoreNetworkService extends Network {
 
                 const hadOfflineMessage = CoreDomUtils.hasModeClass('core-offline');
 
-                CoreDomUtils.toggleModeClass('core-offline', !isOnline, { includeLegacy: true });
+                CoreDomUtils.toggleModeClass('core-offline', !isOnline);
 
                 if (isOnline && hadOfflineMessage) {
-                    CoreDomUtils.toggleModeClass('core-online', true, { includeLegacy: true });
+                    CoreDomUtils.toggleModeClass('core-online', true);
 
                     setTimeout(() => {
-                        CoreDomUtils.toggleModeClass('core-online', false, { includeLegacy: true });
+                        CoreDomUtils.toggleModeClass('core-online', false);
                     }, 3000);
                 } else if (!isOnline) {
-                    CoreDomUtils.toggleModeClass('core-online', false, { includeLegacy: true });
+                    CoreDomUtils.toggleModeClass('core-online', false);
                 }
             });
         });
 
         const isOnline = this.isOnline();
-        CoreDomUtils.toggleModeClass('core-offline', !isOnline, { includeLegacy: true });
+        CoreDomUtils.toggleModeClass('core-offline', !isOnline);
     }
 
     /**
diff --git a/src/core/services/tests/sites.test.ts b/src/core/services/tests/sites.test.ts
index 3147be247..1e91991af 100644
--- a/src/core/services/tests/sites.test.ts
+++ b/src/core/services/tests/sites.test.ts
@@ -51,7 +51,7 @@ describe('CoreSitesProvider', () => {
         CoreHTMLClasses.initialize();
         CoreSites.initialize();
 
-        expect(document.documentElement.classList.contains('ionic7')).toBe(true);
+        expect(document.documentElement.classList.contains('ionic8')).toBe(true);
 
         const site = mock(new CoreSite('42', siteUrl, 'token', { info: {
                 sitename: 'Example Campus',
diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index a1797cf7f..3ea323352 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -1790,17 +1790,12 @@ export class CoreDomUtilsProvider {
      *
      * @param className Class name.
      * @param enable Whether to add or remove the class.
-     * @param options Legacy options, deprecated since 4.1.
      */
     toggleModeClass(
         className: string,
         enable = false,
-        options: { includeLegacy: boolean } = { includeLegacy: false },
     ): void {
         document.documentElement.classList.toggle(className, enable);
-
-        // @deprecated since 4.1.
-        document.body.classList.toggle(className, enable && options.includeLegacy);
     }
 
 }
diff --git a/src/core/services/utils/time.ts b/src/core/services/utils/time.ts
index 24cb2857c..b2992aeea 100644
--- a/src/core/services/utils/time.ts
+++ b/src/core/services/utils/time.ts
@@ -215,19 +215,6 @@ export class CoreTimeUtilsProvider {
         return isoString.substring(0, isoString.indexOf('.'));
     }
 
-    /**
-     * Convert a text into user timezone timestamp.
-     *
-     * @param date To convert to timestamp.
-     * @param applyOffset Whether to apply offset to date or not.
-     * @returns Converted timestamp.
-     * @deprecated since 4.1. Use moment(date).unix() instead.
-     */
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    convertToTimestamp(date: string, applyOffset?: boolean): number {
-        return moment(date).unix();
-    }
-
     /**
      * Return the localized ISO format (i.e DDMMYY) from the localized moment format. Useful for translations.
      * DO NOT USE this function for ion-datetime format. Moment escapes characters with [], but ion-datetime doesn't support it.
diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts
index e2c4b89a4..1a6cc9a28 100644
--- a/src/core/services/utils/utils.ts
+++ b/src/core/services/utils/utils.ts
@@ -1185,7 +1185,7 @@ export class CoreUtilsProvider {
         if (options.showBrowserWarning || options.showBrowserWarning === undefined) {
             try {
                 await CoreWindow.confirmOpenBrowserIfNeeded(originaUrl);
-            } catch (error) {
+            } catch {
                 return; // Cancelled, stop.
             }
         }
@@ -1409,16 +1409,6 @@ export class CoreUtilsProvider {
         return Object.keys(enumeration).filter(k => Number.isNaN(+k)) as K[];
     }
 
-    /**
-     * Create a deferred promise that can be resolved or rejected explicitly.
-     *
-     * @returns The deferred promise.
-     * @deprecated since 4.1. Use CorePromisedValue instead.
-     */
-    promiseDefer<T>(): CorePromisedValue<T> {
-        return new CorePromisedValue<T>();
-    }
-
     /**
      * Given a promise, returns true if it's rejected or false if it's resolved.
      *
diff --git a/src/core/singletons/array.ts b/src/core/singletons/array.ts
index 49f81195f..71e885cb7 100644
--- a/src/core/singletons/array.ts
+++ b/src/core/singletons/array.ts
@@ -17,21 +17,6 @@
  */
 export class CoreArray {
 
-    /**
-     * Check whether an array contains an item.
-     *
-     * @param arr Array.
-     * @param item Item.
-     * @returns Whether item is within the array.
-     * @deprecated since 4.1. Use arr.includes() instead.
-     */
-    static contains<T>(arr: T[], item: T): boolean {
-        // eslint-disable-next-line no-console
-        console.warn('CoreArray.contains is deprecated and will be removed soon. Please use array \'includes\' instead.');
-
-        return arr.indexOf(item) !== -1;
-    }
-
     /**
      * Flatten the first dimension of a multi-dimensional array.
      *
diff --git a/src/core/singletons/components-registry.ts b/src/core/singletons/components-registry.ts
deleted file mode 100644
index 092c66ee6..000000000
--- a/src/core/singletons/components-registry.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-// (C) Copyright 2015 Moodle Pty Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import { Component } from '@angular/core';
-import { AsyncDirective } from '@classes/async-directive';
-import { CoreDirectivesRegistry } from '@singletons/directives-registry';
-
-/**
- * Registry to keep track of component instances.
- *
- * @deprecated since 4.1.1. Use CoreDirectivesRegistry instead.
- */
-export class CoreComponentsRegistry {
-
-    /**
-     * Register a component instance.
-     *
-     * @param element Root element.
-     * @param instance Component instance.
-     */
-    static register(element: Element, instance: unknown): void {
-        CoreDirectivesRegistry.register(element, instance);
-    }
-
-    /**
-     * Resolve a component instance.
-     *
-     * @param element Root element.
-     * @param componentClass Component class.
-     * @returns Component instance.
-     */
-    static resolve<T>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null {
-        return CoreDirectivesRegistry.resolve(element, componentClass);
-    }
-
-    /**
-     * Get a component instances and fail if it cannot be resolved.
-     *
-     * @param element Root element.
-     * @param componentClass Component class.
-     * @returns Component instance.
-     */
-    static require<T>(element: Element, componentClass?: ComponentConstructor<T>): T {
-        return CoreDirectivesRegistry.require(element, componentClass);
-    }
-
-    /**
-     * Get a component instances and wait to be ready.
-     *
-     * @param element Root element.
-     * @param componentClass Component class.
-     * @returns Promise resolved when done.
-     */
-    static async waitComponentReady<T extends AsyncDirective>(
-        element: Element | null,
-        componentClass?: ComponentConstructor<T>,
-    ): Promise<void> {
-        return CoreDirectivesRegistry.waitDirectiveReady(element, componentClass);
-    }
-
-    /**
-     * Waits all elements matching to be ready.
-     *
-     * @param element Element where to search.
-     * @param selector Selector to search on parent.
-     * @param componentClass Component class.
-     * @returns Promise resolved when done.
-     */
-    static async waitComponentsReady<T extends AsyncDirective>(
-        element: Element,
-        selector: string,
-        componentClass?: ComponentConstructor<T>,
-    ): Promise<void> {
-        return CoreDirectivesRegistry.waitDirectivesReady(element, selector, componentClass);
-    }
-
-}
-
-/**
- * Component constructor.
- */
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type ComponentConstructor<T = Component> = { new(...args: any[]): T };
diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts
index ff22077c0..07c0c425e 100644
--- a/src/core/singletons/dom.ts
+++ b/src/core/singletons/dom.ts
@@ -535,20 +535,6 @@ export class CoreDom {
         );
     }
 
-    /**
-     * Listen to click and Enter/Space keys in an element.
-     *
-     * @param element Element to listen to events.
-     * @param callback Callback to call when clicked or the key is pressed.
-     * @deprecated since 4.1.1: Use initializeClickableElementA11y instead.
-     */
-    static onActivate(
-        element: HTMLElement & {disabled?: boolean},
-        callback: (event: MouseEvent | KeyboardEvent) => void,
-    ): void {
-        this.initializeClickableElementA11y(element, callback);
-    }
-
     /**
      * Initializes a clickable element a11y calling the click action when pressed enter or space
      * and adding tabindex and role if needed.
diff --git a/src/core/singletons/html-classes.ts b/src/core/singletons/html-classes.ts
index ca348cd7c..ff74fda5b 100644
--- a/src/core/singletons/html-classes.ts
+++ b/src/core/singletons/html-classes.ts
@@ -34,7 +34,7 @@ export class CoreHTMLClasses {
      * Initialize HTML classes.
      */
     static initialize(): void {
-        CoreDomUtils.toggleModeClass('ionic7', true);
+        CoreDomUtils.toggleModeClass('ionic8', true);
         CoreDomUtils.toggleModeClass('development', CoreConstants.BUILD.isDevelopment);
         CoreHTMLClasses.addVersionClass(MOODLEAPP_VERSION_PREFIX, CoreConstants.CONFIG.versionname.replace('-dev', ''));
 
@@ -72,9 +72,9 @@ export class CoreHTMLClasses {
         parts[1] = parts[1] || '0';
         parts[2] = parts[2] || '0';
 
-        CoreDomUtils.toggleModeClass(prefix + parts[0], true, { includeLegacy: true });
-        CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1], true, { includeLegacy: true });
-        CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true, { includeLegacy: true });
+        CoreDomUtils.toggleModeClass(prefix + parts[0], true);
+        CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1], true);
+        CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true);
     }
 
     /**
@@ -88,7 +88,7 @@ export class CoreHTMLClasses {
                 continue;
             }
 
-            CoreDomUtils.toggleModeClass(modeClass, false, { includeLegacy: true });
+            CoreDomUtils.toggleModeClass(modeClass, false);
         }
     }
 
diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts
index 93fd9580c..05768f8fc 100644
--- a/src/core/singletons/text.ts
+++ b/src/core/singletons/text.ts
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { CorePath } from './path';
-
 /**
  * Singleton with helper functions for text manipulation.
  */
@@ -70,16 +68,4 @@ export class CoreText {
         return text.substring(1);
     }
 
-    /**
-     * Concatenate two paths, adding a slash between them if needed.
-     *
-     * @param leftPath Left path.
-     * @param rightPath Right path.
-     * @returns Concatenated path.
-     * @deprecated since 4.1. Use CorePath.concatenatePaths instead.
-     */
-    static concatenatePaths(leftPath: string, rightPath: string): string {
-        return CorePath.concatenatePaths(leftPath, rightPath);
-    }
-
 }
diff --git a/src/testing/services/behat-blocking.ts b/src/testing/services/behat-blocking.ts
index 57329ba71..716ed4438 100644
--- a/src/testing/services/behat-blocking.ts
+++ b/src/testing/services/behat-blocking.ts
@@ -196,10 +196,13 @@ export class TestingBehatBlockingService {
         await CoreUtils.nextTick();
 
         const blockingElements = Array.from(
-            document.querySelectorAll<HTMLElement>('div.core-loading-container, ion-loading, .click-block-active'),
+            document.querySelectorAll<HTMLElement>('div.core-loading-container, ion-loading'),
         );
 
         const isBlocked = blockingElements.some(element => {
+            // @TODO Fix ion-loading present check with CoreDom.isElementVisible.
+            // ion-loading never has offsetParent since position is fixed.
+            // Using isElementVisible solve the problem but will block behats (like BBB).
             if (!element.offsetParent) {
                 return false;
             }
diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts
index 265d7e3de..c030715a6 100644
--- a/src/testing/services/behat-runtime.ts
+++ b/src/testing/services/behat-runtime.ts
@@ -159,11 +159,19 @@ export class TestingBehatRuntimeService {
      */
     async waitLoadingToFinish(): Promise<void> {
         await NgZone.run(async () => {
-            const elements = Array.from(document.body.querySelectorAll<HTMLElement>('core-loading'))
-                .filter((element) => CoreDom.isElementVisible(element));
+            const coreLoadingsPromises: Promise<unknown>[] =
+            Array.from(document.body.querySelectorAll<HTMLElement>('core-loading'))
+                .filter((element) => CoreDom.isElementVisible(element))
+                .map(element => CoreDirectivesRegistry.waitDirectiveReady(element, CoreLoadingComponent));
 
-            await Promise.all(elements.map(element =>
-                CoreDirectivesRegistry.waitDirectiveReady(element, CoreLoadingComponent)));
+            const ionLoadingsPromises: Promise<unknown>[] =
+            Array.from(document.body.querySelectorAll<HTMLIonLoadingElement>('ion-loading'))
+                .filter((element) => CoreDom.isElementVisible(element))
+                .map(element => element.onDidDismiss());
+
+            const promises = coreLoadingsPromises.concat(ionLoadingsPromises);
+
+            await Promise.all(promises);
         });
     }
 
diff --git a/src/theme/components/error-accordion.scss b/src/theme/components/error-accordion.scss
index 26267c72f..c3691243b 100644
--- a/src/theme/components/error-accordion.scss
+++ b/src/theme/components/error-accordion.scss
@@ -143,6 +143,6 @@
 
 }
 
-html.dark .core-error-accordion {
+:root.dark .core-error-accordion {
     --background-color: var(--gray-700);
 }
diff --git a/src/theme/components/format-text.scss b/src/theme/components/format-text.scss
index 944b205b5..a4740ee59 100644
--- a/src/theme/components/format-text.scss
+++ b/src/theme/components/format-text.scss
@@ -8,7 +8,7 @@ core-format-text {
     --core-format-text-viewer-icon-background: rgb(255 255 255 / 50%);
 }
 
-html.dark core-format-text {
+:root.dark core-format-text {
     --core-format-text-viewer-icon-background: rgb(0 0 0 / 50%);
 }
 
@@ -378,7 +378,7 @@ ion-header.ios h1 core-format-text {
     }
 }
 
-html.dark core-format-text select,
-html.dark core-rich-text-editor .core-rte-editor select {
+:root.dark core-format-text select,
+:root.dark core-rich-text-editor .core-rte-editor select {
 	background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
 }
diff --git a/src/theme/components/ion-fab.scss b/src/theme/components/ion-fab.scss
index f6eee8464..9adfb69c3 100644
--- a/src/theme/components/ion-fab.scss
+++ b/src/theme/components/ion-fab.scss
@@ -6,25 +6,6 @@ ion-fab[core-fab] {
     }
 }
 
-// The following 4 selectors can probably be removed after Ionic migration to 7+
-ion-fab.fab-horizontal-start {
-    left: calc(10px + var(--ion-safe-area-right, 0px));
-}
-
-&[dir=rtl] ion-fab.fab-horizontal-start {
-    right: calc(10px + var(--ion-safe-area-right, 0px));
-    left: unset
-}
-
-ion-fab.fab-horizontal-end {
-    right: calc(10px + var(--ion-safe-area-right, 0px));
-}
-
-&[dir=rtl] ion-fab.fab-horizontal-end {
-    left: calc(10px + var(--ion-safe-area-right, 0px));
-    right: unset
-}
-
 ion-content.has-collapsible-footer ion-fab {
     bottom: calc(var(--core-collapsible-footer-height, 0px) + 10px);
     @include core-transition(all, 200ms);
diff --git a/src/theme/components/ion-input.scss b/src/theme/components/ion-input.scss
index 24fef008b..3db675178 100644
--- a/src/theme/components/ion-input.scss
+++ b/src/theme/components/ion-input.scss
@@ -2,6 +2,10 @@ ion-input {
     &.input-disabled.md, &.input-disabled.ios {
         opacity: var(--mdl-input-disabled-opacity);
     }
+
+    ion-input-password-toggle {
+        --ion-color-primary: var(--text-color);
+    }
 }
 
 input[disabled],
diff --git a/src/theme/components/ion-select.scss b/src/theme/components/ion-select.scss
index 7c177c9b1..37b2f21a2 100644
--- a/src/theme/components/ion-select.scss
+++ b/src/theme/components/ion-select.scss
@@ -15,10 +15,6 @@ ion-select {
 }
 
 ion-select-popover {
-    ion-list ion-radio-group ion-item.select-interface-option ion-radio.hydrated::part(container) {
-        opacity: 1;
-    }
-
     ion-item {
         font-size: var(--text-size);
     }
diff --git a/src/theme/components/ion-toast.scss b/src/theme/components/ion-toast.scss
index e14be047a..f4aea789a 100644
--- a/src/theme/components/ion-toast.scss
+++ b/src/theme/components/ion-toast.scss
@@ -1,9 +1,9 @@
 @import "../globals.scss";
 
 ion-toast {
-    --color: var(--ion-color-step-50);
+    --color: var(--ion-text-color-step-950);
     --button-color: var(--primary);
-    --background: var(--ion-color-step-800);
+    --background: var(--ion-background-color-step-800);
 
     @include media-breakpoint-down(sm) {
         &::part(container) {
diff --git a/src/theme/components/swiper.scss b/src/theme/components/swiper.scss
index f32ffc92e..7a3d66ebb 100644
--- a/src/theme/components/swiper.scss
+++ b/src/theme/components/swiper.scss
@@ -1,7 +1,7 @@
 
 swiper-container {
     --swiper-theme-color: var(--ion-color-primary, #3880ff);
-    --swiper-pagination-bullet-inactive-color: var(--ion-color-step-200, #cccccc);
+    --swiper-pagination-bullet-inactive-color: var(--ion-text-color-step-800, #cccccc);
     --swiper-pagination-color: var(--swiper-theme-color);
     --swiper-pagination-progressbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.25);
     --swiper-scrollbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.1);
diff --git a/src/theme/helpers/custom.mixins.scss b/src/theme/helpers/custom.mixins.scss
index e15794e90..a14d152f8 100644
--- a/src/theme/helpers/custom.mixins.scss
+++ b/src/theme/helpers/custom.mixins.scss
@@ -297,7 +297,7 @@
 @mixin darkmode() {
     $root: #{&};
 
-    @at-root #{add-root-selector($root, "html.dark")} {
+    @at-root #{add-root-selector($root, ":root.dark")} {
         @content;
     }
 }
diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss
index a0f0d9694..2c4350d3e 100644
--- a/src/theme/theme.base.scss
+++ b/src/theme/theme.base.scss
@@ -1,13 +1,13 @@
 @import "./globals.scss";
 
-html.force-safe-area-margins {
+:root.force-safe-area-margins {
     --ion-safe-area-left: 40px;
     --ion-safe-area-right: 40px;
     --ion-safe-area-top: 40px;
     --ion-safe-area-bottom: 40px;
 }
 
-html {
+:root {
     --zoom-level: 100%;
     zoom: var(--zoom-level);
 
@@ -21,7 +21,7 @@ a {
     cursor: pointer;
 }
 
-html[dir=rtl] {
+:root[dir=rtl] {
     --rotate-expandable: rotate(-90deg);
 }
 
@@ -488,7 +488,7 @@ video::-webkit-media-text-track-display {
 /**
  * https://github.com/ionic-team/ionic-framework/blob/6ffbdbb3b2b69290cf25753d535bc7483bd7c6e8/BREAKING.md#css-utilities
  */
-[hidden] {
+[hidden], .hidden {
     display: none !important;
 }
 
diff --git a/src/theme/theme.custom.scss b/src/theme/theme.custom.scss
index 282ff6aaf..77c14d33e 100644
--- a/src/theme/theme.custom.scss
+++ b/src/theme/theme.custom.scss
@@ -8,7 +8,7 @@
  * Light Theme
  * -------------------------------------------
  */
-html {
+:root {
 
 }
 
@@ -16,6 +16,6 @@ html {
  * Dark Theme
  * -------------------------------------------
  */
-html.dark {
+:root.dark {
 
 }
diff --git a/src/theme/theme.dark.scss b/src/theme/theme.dark.scss
index f2836b6b9..e303ae237 100644
--- a/src/theme/theme.dark.scss
+++ b/src/theme/theme.dark.scss
@@ -5,171 +5,198 @@
  * http://ionicframework.com/docs/theming/
  */
 
-html.dark {
+@mixin dark-palette() {
+    & {
+        // Ionic shades, defined for ionic internal use.
+        --ion-background-color-step-0:    var(--black);
+        --ion-background-color-step-50:   var(--gray-900);
+        --ion-background-color-step-100:  var(--gray-900);
+        --ion-background-color-step-150:  var(--gray-800);
+        --ion-background-color-step-200:  var(--gray-800);
+        --ion-background-color-step-250:  var(--gray-700);
+        --ion-background-color-step-300:  var(--gray-700);
+        --ion-background-color-step-350:  var(--gray-600);
+        --ion-background-color-step-400:  var(--gray-600);
+        --ion-background-color-step-450:  var(--gray-500);
+        --ion-background-color-step-500:  var(--gray-500);
+        --ion-background-color-step-550:  var(--gray-400);
+        --ion-background-color-step-600:  var(--gray-400);
+        --ion-background-color-step-650:  var(--gray-300);
+        --ion-background-color-step-700:  var(--gray-300);
+        --ion-background-color-step-750:  var(--gray-200);
+        --ion-background-color-step-800:  var(--gray-200);
+        --ion-background-color-step-850:  var(--gray-100);
+        --ion-background-color-step-900:  var(--gray-100);
+        --ion-background-color-step-950:  var(--gray-100);
+        --ion-background-color-step-1000: var(--white);
 
-    // Ionic shades, defined for ionic internal use.
-    --ion-color-step-0:    var(--black);
-    --ion-color-step-50:   var(--gray-900);
-    --ion-color-step-100:  var(--gray-900);
-    --ion-color-step-150:  var(--gray-800);
-    --ion-color-step-200:  var(--gray-800);
-    --ion-color-step-250:  var(--gray-700);
-    --ion-color-step-300:  var(--gray-700);
-    --ion-color-step-350:  var(--gray-600);
-    --ion-color-step-400:  var(--gray-600);
-    --ion-color-step-450:  var(--gray-500);
-    --ion-color-step-500:  var(--gray-500);
-    --ion-color-step-550:  var(--gray-400);
-    --ion-color-step-600:  var(--gray-400);
-    --ion-color-step-650:  var(--gray-300);
-    --ion-color-step-700:  var(--gray-300);
-    --ion-color-step-750:  var(--gray-200);
-    --ion-color-step-800:  var(--gray-200);
-    --ion-color-step-850:  var(--gray-100);
-    --ion-color-step-900:  var(--gray-100);
-    --ion-color-step-950:  var(--gray-100);
-    --ion-color-step-1000: var(--white);
+        --ion-text-color-step-0:    var(--white);
+        --ion-text-color-step-50:   var(--gray-100);
+        --ion-text-color-step-100:  var(--gray-100);
+        --ion-text-color-step-150:  var(--gray-200);
+        --ion-text-color-step-200:  var(--gray-200);
+        --ion-text-color-step-250:  var(--gray-300);
+        --ion-text-color-step-300:  var(--gray-300);
+        --ion-text-color-step-350:  var(--gray-400);
+        --ion-text-color-step-400:  var(--gray-400);
+        --ion-text-color-step-450:  var(--gray-500);
+        --ion-text-color-step-500:  var(--gray-500);
+        --ion-text-color-step-550:  var(--gray-600);
+        --ion-text-color-step-600:  var(--gray-600);
+        --ion-text-color-step-650:  var(--gray-700);
+        --ion-text-color-step-700:  var(--gray-700);
+        --ion-text-color-step-750:  var(--gray-800);
+        --ion-text-color-step-800:  var(--gray-800);
+        --ion-text-color-step-850:  var(--gray-900);
+        --ion-text-color-step-900:  var(--gray-900);
+        --ion-text-color-step-950:  var(--gray-900);
+        --ion-text-color-step-1000: var(--black);
 
-    @each $color-name, $unused in $colors {
-        @include generate-color($color-name, $colors, 'dark');
-    }
-
-    --ion-background-color:     #{$background-color-dark};
-    --ion-background-color-rgb: #{$background-color-dark-rgb};
-
-    --text-color:               #{$text-color-dark};
-    --ion-text-color:           var(--text-color);
-    --ion-text-color-rgb:       #{$text-color-dark-rgb};
-    --subdued-text-color:       var(--medium);
-    --stroke:                   var(--gray-700);
-
-    --contrast-background: var(--gray-900);
-
-    --loader-shine: 90 90 90;
-
-    --drop-shadow-top: 0px 2px 5px rgb(var(--mdl-shadow-boxShadowColor) / 1);
-    --drop-shadow-bottom: 0px -2px 5px rgb(var(--mdl-shadow-boxShadowColor) / 1);
-
-    --ion-card-color: var(--text-color);
-    --ion-card-background: var(--ion-item-background);
-    --ion-card-border-color: var(--stroke);
-
-    --ion-border-color: var(--stroke);
-    --ion-item-border-color: var(--stroke);
-
-    --core-input-stroke: var(--gray-600);
-    --core-input-text: var(--dark);
-    --core-input-background: var(--gray-900);
-
-    ion-content {
-        --background: var(--ion-background-color);
-        --background-alternative: var(--gray-900);
-    }
-
-    --core-bottom-tabs-badge-text-color: var(--primary-contrast);
-    --core-bottom-tabs-background: var(--gray-900);
-    --core-bottom-tabs-color: var(--gray-200);
-
-    ion-action-sheet {
-        .action-sheet-cancel {
-            --button-color: var(--danger-tint);
+        @each $color-name, $unused in $colors {
+            @include generate-color($color-name, $colors, 'dark');
         }
+
+        --ion-background-color:     #{$background-color-dark};
+        --ion-background-color-rgb: #{$background-color-dark-rgb};
+
+        --text-color:               #{$text-color-dark};
+        --ion-text-color:           var(--text-color);
+        --ion-text-color-rgb:       #{$text-color-dark-rgb};
+        --subdued-text-color:       var(--medium);
+        --stroke:                   var(--gray-700);
+
+        --contrast-background: var(--gray-900);
+
+        --loader-shine: 90 90 90;
+
+        --drop-shadow-top: 0px 2px 5px rgb(var(--mdl-shadow-boxShadowColor) / 1);
+        --drop-shadow-bottom: 0px -2px 5px rgb(var(--mdl-shadow-boxShadowColor) / 1);
+
+        --ion-card-color: var(--text-color);
+        --ion-card-background: var(--ion-item-background);
+        --ion-card-border-color: var(--stroke);
+
+        --ion-border-color: var(--stroke);
+        --ion-item-border-color: var(--stroke);
+
+        --core-input-stroke: var(--gray-600);
+        --core-input-text: var(--dark);
+        --core-input-background: var(--gray-900);
+
+        ion-content {
+            --background: var(--ion-background-color);
+            --background-alternative: var(--gray-900);
+        }
+
+        --core-bottom-tabs-badge-text-color: var(--primary-contrast);
+        --core-bottom-tabs-background: var(--gray-900);
+        --core-bottom-tabs-color: var(--gray-200);
+
+        ion-action-sheet {
+            .action-sheet-cancel {
+                --button-color: var(--danger-tint);
+            }
+        }
+
+        ion-popover,
+        ion-popover ion-content {
+            --background: var(--gray-800);
+            --ion-item-background: var(--background);
+            --ion-background-color: var(--background);
+        }
+
+        --core-link-color: var(--info-tint);
+
+        --core-header-toolbar-background:   var(--gray-900);
+        --core-header-toolbar-color:        var(--text-color);
+        --core-header-toolbar-border-color: var(--stroke);
+
+        --core-tabs-background: var(--gray-900);
+        --core-tab-background: var(--core-tabs-background);
+        --core-tab-color: var(--subdued-text-color);
+        --core-tab-border-color: var(--gray-200);
+        --core-tab-color-active: var(--dark);
+
+        --core-progressbar-text-color: var(--gray-100);
+
+        --core-empty-box-icon-color: var(--gray-700);
+
+        --ion-item-background: #{$ion-item-background-dark};
+        --ion-item-icon-color: var(--medium);
+        --ion-item-detail-icon-color: var(--dark);
+        --core-more-icon: var(--ion-item-icon-color);
+
+        --item-divider-background: var(--ion-item-background);
+        --item-divider-color: var(--text-color);
+        --spacer-background: var(--gray-700);
+
+
+        --ion-searchbar-background: var(--ion-background-color);
+        --ion-searchbar-border-color: var(--core-input-stroke);
+        --ion-searchbar-color: var(--text-color);
+        --ion-searchbar-icon-color: var(--core-input-stroke);
+
+        --core-search-box-background: var(--ion-background-color);
+        --core-search-box-border-color: var(--core-input-stroke);
+        --core-search-box-color: var(--core-input-text);
+
+        --core-combobox-background: var(--core-input-background);
+        --core-combobox-color: var(--text-color);
+        --core-combobox-border-color: var(--core-input-stroke);
+
+        --collapsible-toggle-text: var(--medium);
+
+        --background-gradient-rgb: #{$ion-item-background-dark-rgb};
+
+        --core-login-background: var(--gray-900);
+        --core-login-text-color: var(--white);
+        --core-login-input-background: var(--core-login-background);
+        --core-login-input-color: var(--core-login-text-color);
+
+        --core-question-correct-color: var(--success-tint);
+        --core-question-correct-color-bg: var(--success-shade);
+        --core-question-incorrect-color: var(--danger);
+        --core-question-incorrect-color-bg: var(--danger-shade);
+        --core-question-partial-color: var(--warning-tint);
+        --core-question-partial-color-bg: var(--warning-shade);
+        --core-question-feedback-color: var(--warning-tint);
+        --core-question-feedback-color-bg: var(--warning-shade);
+        --core-question-warning-color: var(--danger);
+        --core-question-saved-color-bg: var(--gray-500);
+
+        --core-question-feedback-color: var(--warning-tint);
+        --core-question-feedback-background-color: var(--warning-shade);
+
+        --core-dd-question-selected-shadow: 2px 2px 4px var(--gray-200);
+        --core-dd-question-border: var(--gray-200);
+
+        --core-send-message-input-background: var(--gray-900);
+        --core-send-message-input-color: var(--white);
+
+        --core-navigation-background: var(--contrast-background);
+
+        --core-collapsible-footer-background: var(--contrast-background);
+
+        --core-messages-message-bg: var(--gray-800);
+        --core-messages-message-activated-bg: var(--gray-700);
+        --core-messages-message-note-text: var(--subdued-text-color);
+        --core-messages-message-mine-bg: var(--gray-700);
+        --core-messages-message-mine-activated-bg: var(--gray-600);
+        --core-messages-discussion-badge: var(--primary);
+        --core-messages-discussion-badge-text: var(--gray-100);
+
+        --addon-forum-border-color: var(--gray-500);
+        --addon-forum-highlight-color: var(--gray-800);
+
+        --core-table-header-background: var(--gray-900);
+        --core-table-odd-cell-background: var(--gray-800);
+        --core-table-odd-cell-hover: var(--gray-600);
+        --core-table-even-cell-background: var(--gray-900);
+        --core-table-even-cell-hover: var(--gray-700);
+
     }
-
-    ion-popover,
-    ion-popover ion-content {
-        --background: var(--gray-800);
-        --ion-item-background: var(--background);
-        --ion-background-color: var(--background);
-    }
-
-    --core-link-color: var(--info-tint);
-
-    --core-header-toolbar-background:   var(--gray-900);
-    --core-header-toolbar-color:        var(--text-color);
-    --core-header-toolbar-border-color: var(--stroke);
-
-    --core-tabs-background: var(--gray-900);
-    --core-tab-background: var(--core-tabs-background);
-    --core-tab-color: var(--subdued-text-color);
-    --core-tab-border-color: var(--gray-200);
-    --core-tab-color-active: var(--dark);
-
-    --core-progressbar-text-color: var(--gray-100);
-
-    --core-empty-box-icon-color: var(--gray-700);
-
-    --ion-item-background: #{$ion-item-background-dark};
-    --ion-item-icon-color: var(--medium);
-    --ion-item-detail-icon-color: var(--dark);
-    --core-more-icon: var(--ion-item-icon-color);
-
-    --item-divider-background: var(--ion-item-background);
-    --item-divider-color: var(--text-color);
-    --spacer-background: var(--gray-700);
-
-
-    --ion-searchbar-background: var(--ion-background-color);
-    --ion-searchbar-border-color: var(--core-input-stroke);
-    --ion-searchbar-color: var(--text-color);
-    --ion-searchbar-icon-color: var(--core-input-stroke);
-
-    --core-search-box-background: var(--ion-background-color);
-    --core-search-box-border-color: var(--core-input-stroke);
-    --core-search-box-color: var(--core-input-text);
-
-    --core-combobox-background: var(--core-input-background);
-    --core-combobox-color: var(--text-color);
-    --core-combobox-border-color: var(--core-input-stroke);
-
-    --collapsible-toggle-text: var(--medium);
-
-    --background-gradient-rgb: #{$ion-item-background-dark-rgb};
-
-    --core-login-background: var(--gray-900);
-    --core-login-text-color: var(--white);
-    --core-login-input-background: var(--core-login-background);
-    --core-login-input-color: var(--core-login-text-color);
-
-    --core-question-correct-color: var(--success-tint);
-    --core-question-correct-color-bg: var(--success-shade);
-    --core-question-incorrect-color: var(--danger);
-    --core-question-incorrect-color-bg: var(--danger-shade);
-    --core-question-partial-color: var(--warning-tint);
-    --core-question-partial-color-bg: var(--warning-shade);
-    --core-question-feedback-color: var(--warning-tint);
-    --core-question-feedback-color-bg: var(--warning-shade);
-    --core-question-warning-color: var(--danger);
-    --core-question-saved-color-bg: var(--gray-500);
-
-    --core-question-feedback-color: var(--warning-tint);
-    --core-question-feedback-background-color: var(--warning-shade);
-
-    --core-dd-question-selected-shadow: 2px 2px 4px var(--gray-200);
-    --core-dd-question-border: var(--gray-200);
-
-    --core-send-message-input-background: var(--gray-900);
-    --core-send-message-input-color: var(--white);
-
-    --core-navigation-background: var(--contrast-background);
-
-    --core-collapsible-footer-background: var(--contrast-background);
-
-    --core-messages-message-bg: var(--gray-800);
-    --core-messages-message-activated-bg: var(--gray-700);
-    --core-messages-message-note-text: var(--subdued-text-color);
-    --core-messages-message-mine-bg: var(--gray-700);
-    --core-messages-message-mine-activated-bg: var(--gray-600);
-    --core-messages-discussion-badge: var(--primary);
-    --core-messages-discussion-badge-text: var(--gray-100);
-
-    --addon-forum-border-color: var(--gray-500);
-    --addon-forum-highlight-color: var(--gray-800);
-
-    --core-table-header-background: var(--gray-900);
-    --core-table-odd-cell-background: var(--gray-800);
-    --core-table-odd-cell-hover: var(--gray-600);
-    --core-table-even-cell-background: var(--gray-900);
-    --core-table-even-cell-hover: var(--gray-700);
-
+}
+
+:root.dark {
+    @include dark-palette();
 }
diff --git a/src/theme/theme.design-system.scss b/src/theme/theme.design-system.scss
index f57935c52..ba3453db4 100644
--- a/src/theme/theme.design-system.scss
+++ b/src/theme/theme.design-system.scss
@@ -2,7 +2,7 @@
 @import "theme.light.scss";
 @import "theme.dark.scss";
 
-html {
+:root {
     // Add serif fallback font for km language in some devices.
     --ion-font-family: var(--ion-default-font), serif;
 
@@ -243,7 +243,7 @@ html {
 }
 
 /** @deprecated since 4.3 **/
-html {
+:root {
     --small-radius: var(--mdl-shape-borderRadius-xs);
     --medium-radius: var(--mdl-shape-borderRadius-sm);
     --big-radius: var(--mdl-shape-borderRadius-lg);
@@ -251,7 +251,7 @@ html {
 }
 
 /** @deprecated since 4.4 **/
-html {
+:root {
     --font-size-normal: var(--mdl-typography-fontSize-md);
     --a11y-min-target-size: var(--a11y-sizing-minTargetSize);
     --a11y-focus-width: var(--a11y-shadow-focus-boxShadowSpread);
diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss
index 919eb588d..ed1eb2abf 100644
--- a/src/theme/theme.light.scss
+++ b/src/theme/theme.light.scss
@@ -5,7 +5,7 @@
  * http://ionicframework.com/docs/theming/
  */
 
-html {
+:root {
 
     // Color palette
     --black: #{$black};
@@ -21,27 +21,49 @@ html {
     --white: #{$white};
 
     // Ionic shades, defined for ionic internal use.
-    --ion-color-step-0:    var(--white);
-    --ion-color-step-50:   var(--gray-100);
-    --ion-color-step-100:  var(--gray-100);
-    --ion-color-step-150:  var(--gray-200);
-    --ion-color-step-200:  var(--gray-200);
-    --ion-color-step-250:  var(--gray-300);
-    --ion-color-step-300:  var(--gray-300);
-    --ion-color-step-350:  var(--gray-400);
-    --ion-color-step-400:  var(--gray-400);
-    --ion-color-step-450:  var(--gray-500);
-    --ion-color-step-500:  var(--gray-500);
-    --ion-color-step-550:  var(--gray-600);
-    --ion-color-step-600:  var(--gray-600);
-    --ion-color-step-650:  var(--gray-700);
-    --ion-color-step-700:  var(--gray-700);
-    --ion-color-step-750:  var(--gray-800);
-    --ion-color-step-800:  var(--gray-800);
-    --ion-color-step-850:  var(--gray-900);
-    --ion-color-step-900:  var(--gray-900);
-    --ion-color-step-950:  var(--gray-900);
-    --ion-color-step-1000: var(--black);
+    --ion-background-color-step-0:    var(--white);
+    --ion-background-color-step-50:   var(--gray-100);
+    --ion-background-color-step-100:  var(--gray-100);
+    --ion-background-color-step-150:  var(--gray-200);
+    --ion-background-color-step-200:  var(--gray-200);
+    --ion-background-color-step-250:  var(--gray-300);
+    --ion-background-color-step-300:  var(--gray-300);
+    --ion-background-color-step-350:  var(--gray-400);
+    --ion-background-color-step-400:  var(--gray-400);
+    --ion-background-color-step-450:  var(--gray-500);
+    --ion-background-color-step-500:  var(--gray-500);
+    --ion-background-color-step-550:  var(--gray-600);
+    --ion-background-color-step-600:  var(--gray-600);
+    --ion-background-color-step-650:  var(--gray-700);
+    --ion-background-color-step-700:  var(--gray-700);
+    --ion-background-color-step-750:  var(--gray-800);
+    --ion-background-color-step-800:  var(--gray-800);
+    --ion-background-color-step-850:  var(--gray-900);
+    --ion-background-color-step-900:  var(--gray-900);
+    --ion-background-color-step-950:  var(--gray-900);
+    --ion-background-color-step-1000: var(--black);
+
+    --ion-text-color-step-0:    var(--black);
+    --ion-text-color-step-50:   var(--gray-900);
+    --ion-text-color-step-100:  var(--gray-900);
+    --ion-text-color-step-150:  var(--gray-800);
+    --ion-text-color-step-200:  var(--gray-800);
+    --ion-text-color-step-250:  var(--gray-700);
+    --ion-text-color-step-300:  var(--gray-700);
+    --ion-text-color-step-350:  var(--gray-600);
+    --ion-text-color-step-400:  var(--gray-600);
+    --ion-text-color-step-450:  var(--gray-500);
+    --ion-text-color-step-500:  var(--gray-500);
+    --ion-text-color-step-550:  var(--gray-400);
+    --ion-text-color-step-600:  var(--gray-400);
+    --ion-text-color-step-650:  var(--gray-300);
+    --ion-text-color-step-700:  var(--gray-300);
+    --ion-text-color-step-750:  var(--gray-200);
+    --ion-text-color-step-800:  var(--gray-200);
+    --ion-text-color-step-850:  var(--gray-100);
+    --ion-text-color-step-900:  var(--gray-100);
+    --ion-text-color-step-950:  var(--gray-100);
+    --ion-text-color-step-1000: var(--white);
 
     @each $color-name, $unused in $colors {
         @include generate-color($color-name, $colors, 'light');
diff --git a/src/theme/theme.scss b/src/theme/theme.scss
index 31a41afe7..e35ae118a 100644
--- a/src/theme/theme.scss
+++ b/src/theme/theme.scss
@@ -29,36 +29,34 @@
 @import "components/videojs.scss";
 
 /* Ionic components overrides */
-html {
-    @import "components/ion-accordion.scss";
-    @import "components/ion-action-sheet.scss";
-    @import "components/ion-alert.scss";
-    @import "components/ion-avatar.scss";
-    @import "components/ion-back-button.scss";
-    @import "components/ion-badge.scss";
-    @import "components/ion-button.scss";
-    @import "components/ion-card.scss";
-    @import "components/ion-checkbox.scss";
-    @import "components/ion-chip.scss";
-    @import "components/ion-content.scss";
-    @import "components/ion-datetime.scss";
-    @import "components/ion-fab.scss";
-    @import "components/ion-header.scss";
-    @import "components/ion-icon.scss";
-    @import "components/ion-input.scss";
-    @import "components/ion-item.scss";
-    @import "components/ion-item-divider.scss";
-    @import "components/ion-modal.scss";
-    @import "components/ion-loading.scss";
-    @import "components/ion-note.scss";
-    @import "components/ion-popover.scss";
-    @import "components/ion-radio.scss";
-    @import "components/ion-searchbar.scss";
-    @import "components/ion-select.scss";
-    @import "components/ion-spinner.scss";
-    @import "components/ion-toast.scss";
-    @import "components/swiper.scss";
-}
+@import "components/ion-accordion.scss";
+@import "components/ion-action-sheet.scss";
+@import "components/ion-alert.scss";
+@import "components/ion-avatar.scss";
+@import "components/ion-back-button.scss";
+@import "components/ion-badge.scss";
+@import "components/ion-button.scss";
+@import "components/ion-card.scss";
+@import "components/ion-checkbox.scss";
+@import "components/ion-chip.scss";
+@import "components/ion-content.scss";
+@import "components/ion-datetime.scss";
+@import "components/ion-fab.scss";
+@import "components/ion-header.scss";
+@import "components/ion-icon.scss";
+@import "components/ion-input.scss";
+@import "components/ion-item.scss";
+@import "components/ion-item-divider.scss";
+@import "components/ion-modal.scss";
+@import "components/ion-loading.scss";
+@import "components/ion-note.scss";
+@import "components/ion-popover.scss";
+@import "components/ion-radio.scss";
+@import "components/ion-searchbar.scss";
+@import "components/ion-select.scss";
+@import "components/ion-spinner.scss";
+@import "components/ion-toast.scss";
+@import "components/swiper.scss";
 
 /* Some styles from 3rd party libraries. */
 @import "components/bootstrap/utilities/screenreaders.scss";
@@ -67,14 +65,26 @@ html {
 @import "@ionic/angular/css/core.css";
 
 /* Basic CSS for apps built with Ionic */
-@import "@ionic/angular/css/normalize.css";
-@import "@ionic/angular/css/structure.css";
-@import "@ionic/angular/css/typography.css";
-@import "@ionic/angular/css/display.css";
+@import '@ionic/angular/css/normalize.css';
+@import '@ionic/angular/css/structure.css';
+@import '@ionic/angular/css/typography.css';
 
 /* Optional CSS utils that can be commented out */
-@import "@ionic/angular/css/padding.css";
-@import "@ionic/angular/css/float-elements.css";
-@import "@ionic/angular/css/text-alignment.css";
-@import "@ionic/angular/css/text-transformation.css";
-@import "@ionic/angular/css/flex-utils.css";
+@import '@ionic/angular/css/padding.css';
+@import '@ionic/angular/css/float-elements.css';
+@import '@ionic/angular/css/text-alignment.css';
+@import '@ionic/angular/css/text-transformation.css';
+@import '@ionic/angular/css/flex-utils.css';
+@import '@ionic/angular/css/display.css';
+
+
+/**
+ * Ionic Dark Palette
+ * -----------------------------------------------------
+ * For more information, please see:
+ * https://ionicframework.com/docs/theming/dark-mode
+ */
+
+/* @import '@ionic/angular/css/palettes/dark.always.css'; */
+/* @import '@ionic/angular/css/palettes/dark.class.css'; */
+/* @import '@ionic/angular/css/palettes/dark.system.css'; */
diff --git a/upgrade.txt b/upgrade.txt
index 4ab642a51..edb6425e0 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -2,8 +2,15 @@ This file describes API changes in the Moodle App that affect site plugins, info
 
 For more information about upgrading, read the official documentation: https://moodledev.io/general/app/upgrading/
 
+=== 4.5.0 ===
+
+ - Ionic has been upgraded to major version 8. See breaking changes and upgrade guide here: https://ionicframework.com/docs/updating/8-0
+ - core-show-password has been deprecated in favor of ion-input-password-toggle
+
+
 === 4.4.0 ===
 
+ - Ionic has been upgraded to major version 7. See breaking changes and upgrade guide here: https://ionicframework.com/docs/updating/7-0 and https://ionicframework.com/docs/updating/6-0
  - Starting with this release, this file will only document breaking changes for APIs exposed to site plugins. Internal changes will no longer be documented.
  - CoreCache has been deprecated, use plain object as in-memory stores instead.
  - Renamed CoreLoginSitesComponent to CoreLoginSitesModalComponent to make it clear that it's a modal and to avoid confusing it with the new CoreSitesListComponent.