diff --git a/composer.json b/composer.json
index 850df87..b522dae 100644
--- a/composer.json
+++ b/composer.json
@@ -16,6 +16,6 @@
     "source": "https://git.drupalcode.org/project/css_usage_analyzer"
   },
   "require": {
-    "drupal/core": "^10.3 || ^11"
+    "drupal/core": "^10.3 || ^11 || ^12"
   }
 }
diff --git a/css_usage_analyzer.info.yml b/css_usage_analyzer.info.yml
index 30f7dc5..92c514c 100644
--- a/css_usage_analyzer.info.yml
+++ b/css_usage_analyzer.info.yml
@@ -2,7 +2,7 @@ name: 'CSS Usage Analyzer'
 type: module
 description: 'Detects and reports unused CSS across Drupal themes with per-page analysis, component maps, and critical CSS suggestions.'
 package: 'Frontend Optimization'
-core_version_requirement: ^10.3 || ^11
+core_version_requirement: ^10.3 || ^11 || ^12
 dependencies:
   - drupal:system
   - drupal:user
diff --git a/css_usage_analyzer.module b/css_usage_analyzer.module
index d15572e..b787c4b 100644
--- a/css_usage_analyzer.module
+++ b/css_usage_analyzer.module
@@ -5,17 +5,16 @@
  * CSS Usage Analyzer module — detects unused CSS in Drupal themes.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\css_usage_analyzer\Hook\CssUsageAnalyzerHooks;
 use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function css_usage_analyzer_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.css_usage_analyzer':
-      return '<p>' . t('The <strong>CSS Usage Analyzer</strong> module scans pages in your Drupal site and reports which CSS rules, selectors, and stylesheets are unused, helping you optimise frontend performance.') . '</p>';
-  }
+  return \Drupal::service(CssUsageAnalyzerHooks::class)->help($route_name, $route_match);
 }
 
 /**
@@ -25,71 +24,15 @@ function css_usage_analyzer_help($route_name, RouteMatchInterface $route_match)
  *  (a) auto_scan is enabled in settings, OR
  *  (b) ?css_force_scan=1 is present in the URL (used by the bulk scanner).
  */
+#[LegacyHook]
 function css_usage_analyzer_page_attachments(array &$attachments) {
-  $config     = \Drupal::config('css_usage_analyzer.settings');
-  $is_admin   = \Drupal::service('router.admin_context')->isAdminRoute();
-  $request    = \Drupal::request();
-  $force_scan = $request->query->get('css_force_scan') === '1';
-  $has_perm   = \Drupal::currentUser()->hasPermission('access css usage analyzer');
-
-  // Never run on admin pages.
-  if ($is_admin) {
-    return;
-  }
-
-  // Attach when auto_scan is on OR when force_scan param is present.
-  if ($has_perm && ($force_scan || $config->get('auto_scan'))) {
-    $attachments['#attached']['library'][] = 'css_usage_analyzer/scanner';
-    $attachments['#attached']['drupalSettings']['cssUsageAnalyzer'] = [
-      'saveEndpoint' => Url::fromRoute('css_usage_analyzer.ajax_save')->toString(),
-      'pageUrl'      => $request->getRequestUri(),
-      'enabled'      => TRUE,
-      'forceMode'    => $force_scan,
-      // In force mode always scan (100%), otherwise honour sample rate.
-      'sampleRate'   => $force_scan ? 100 : (int) ($config->get('sample_rate') ?? 100),
-    ];
-  }
+  \Drupal::service(CssUsageAnalyzerHooks::class)->pageAttachments($attachments);
 }
 
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function css_usage_analyzer_theme() {
-  return [
-    'css_usage_dashboard' => [
-      'variables' => [
-        'stats'        => [],
-        'recent_scans' => [],
-        'top_offenders' => [],
-        'history'      => [],
-      ],
-      'template' => 'css-usage-dashboard',
-    ],
-    'css_usage_page_analysis' => [
-      'variables' => [
-        'scan'          => NULL,
-        'stylesheets'   => [],
-        'unused_rules'  => [],
-        'critical_size' => 0,
-        'breakdown'     => [],
-        'all_scans'     => [],
-      ],
-      'template' => 'css-usage-page-analysis',
-    ],
-    'css_usage_component_map' => [
-      'variables' => [
-        'components' => [],
-        'total'      => 0,
-      ],
-      'template' => 'css-usage-component-map',
-    ],
-    'css_usage_bulk_scan' => [
-      'variables' => [
-        'urls'         => [],
-        'save_endpoint' => '',
-        'base_url'     => '',
-      ],
-      'template' => 'css-usage-bulk-scan',
-    ],
-  ];
+  return \Drupal::service(CssUsageAnalyzerHooks::class)->theme();
 }
diff --git a/css_usage_analyzer.services.yml b/css_usage_analyzer.services.yml
index 81bfe18..bc43557 100644
--- a/css_usage_analyzer.services.yml
+++ b/css_usage_analyzer.services.yml
@@ -15,3 +15,7 @@ services:
     class: Drupal\css_usage_analyzer\Service\CssParserService
     arguments:
       - '@config.factory'
+
+  Drupal\css_usage_analyzer\Hook\CssUsageAnalyzerHooks:
+    class: Drupal\css_usage_analyzer\Hook\CssUsageAnalyzerHooks
+    autowire: true
diff --git a/src/Hook/CssUsageAnalyzerHooks.php b/src/Hook/CssUsageAnalyzerHooks.php
new file mode 100644
index 0000000..6dcd13f
--- /dev/null
+++ b/src/Hook/CssUsageAnalyzerHooks.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\css_usage_analyzer\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for css_usage_analyzer.
+ */
+class CssUsageAnalyzerHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.css_usage_analyzer':
+        return '<p>' . $this->t('The <strong>CSS Usage Analyzer</strong> module scans pages in your Drupal site and reports which CSS rules, selectors, and stylesheets are unused, helping you optimise frontend performance.') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_page_attachments().
+   *
+   * Attaches the frontend CSS scanner when:
+   *  (a) auto_scan is enabled in settings, OR
+   *  (b) ?css_force_scan=1 is present in the URL (used by the bulk scanner).
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$attachments) {
+    $config = \Drupal::config('css_usage_analyzer.settings');
+    $is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
+    $request = \Drupal::request();
+    $force_scan = $request->query->get('css_force_scan') === '1';
+    $has_perm = \Drupal::currentUser()->hasPermission('access css usage analyzer');
+    // Never run on admin pages.
+    if ($is_admin) {
+      return;
+    }
+    // Attach when auto_scan is on OR when force_scan param is present.
+    if ($has_perm && ($force_scan || $config->get('auto_scan'))) {
+      $attachments['#attached']['library'][] = 'css_usage_analyzer/scanner';
+      $attachments['#attached']['drupalSettings']['cssUsageAnalyzer'] = [
+        'saveEndpoint' => Url::fromRoute('css_usage_analyzer.ajax_save')->toString(),
+        'pageUrl' => $request->getRequestUri(),
+        'enabled' => TRUE,
+        'forceMode' => $force_scan,
+            // In force mode always scan (100%), otherwise honour sample rate.
+        'sampleRate' => $force_scan ? 100 : (int) ($config->get('sample_rate') ?? 100),
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() {
+    return [
+      'css_usage_dashboard' => [
+        'variables' => [
+          'stats' => [],
+          'recent_scans' => [],
+          'top_offenders' => [],
+          'history' => [],
+        ],
+        'template' => 'css-usage-dashboard',
+      ],
+      'css_usage_page_analysis' => [
+        'variables' => [
+          'scan' => NULL,
+          'stylesheets' => [],
+          'unused_rules' => [],
+          'critical_size' => 0,
+          'breakdown' => [],
+          'all_scans' => [],
+        ],
+        'template' => 'css-usage-page-analysis',
+      ],
+      'css_usage_component_map' => [
+        'variables' => [
+          'components' => [],
+          'total' => 0,
+        ],
+        'template' => 'css-usage-component-map',
+      ],
+      'css_usage_bulk_scan' => [
+        'variables' => [
+          'urls' => [],
+          'save_endpoint' => '',
+          'base_url' => '',
+        ],
+        'template' => 'css-usage-bulk-scan',
+      ],
+    ];
+  }
+
+}
diff --git a/src/Service/CssAnalyzerService.php b/src/Service/CssAnalyzerService.php
index e7288d2..6b487b0 100644
--- a/src/Service/CssAnalyzerService.php
+++ b/src/Service/CssAnalyzerService.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\css_usage_analyzer\Service;
 
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Database\Connection;
@@ -185,7 +187,7 @@ class CssAnalyzerService {
       "SELECT * FROM {css_analyzer_stylesheets} WHERE scan_id = :scan_id ORDER BY unused_bytes DESC",
       [':scan_id' => $scanId]
     );
-    return $query->fetchAll(\PDO::FETCH_ASSOC);
+    return DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $query->fetchAll(FetchAs::Associative), fn() => $query->fetchAll(\PDO::FETCH_ASSOC));
   }
 
   /**
@@ -196,7 +198,7 @@ class CssAnalyzerService {
       "SELECT * FROM {css_analyzer_rules} WHERE scan_id = :scan_id AND is_used = 0 ORDER BY rule_size DESC LIMIT $offset, $limit",
       [':scan_id' => $scanId]
     );
-    return $query->fetchAll(\PDO::FETCH_ASSOC);
+    return DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $query->fetchAll(FetchAs::Associative), fn() => $query->fetchAll(\PDO::FETCH_ASSOC));
   }
 
   /**
@@ -206,7 +208,7 @@ class CssAnalyzerService {
     $query = $this->getDatabase()->query(
       "SELECT * FROM {css_analyzer_scans} ORDER BY created DESC LIMIT $limit"
     );
-    return $query->fetchAll(\PDO::FETCH_ASSOC);
+    return DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $query->fetchAll(FetchAs::Associative), fn() => $query->fetchAll(\PDO::FETCH_ASSOC));
   }
 
   /**
@@ -218,7 +220,7 @@ class CssAnalyzerService {
       "SELECT created, unused_css_bytes, unused_rules FROM {css_analyzer_scans} WHERE created >= :since ORDER BY created ASC",
       [':since' => $since]
     );
-    return $query->fetchAll(\PDO::FETCH_ASSOC);
+    return DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $query->fetchAll(FetchAs::Associative), fn() => $query->fetchAll(\PDO::FETCH_ASSOC));
   }
 
   /**
@@ -228,7 +230,7 @@ class CssAnalyzerService {
     $query = $this->getDatabase()->query(
       "SELECT href, label, total_bytes, unused_bytes, unused_rules FROM {css_analyzer_stylesheets} ORDER BY unused_bytes DESC LIMIT $limit"
     );
-    return $query->fetchAll(\PDO::FETCH_ASSOC);
+    return DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $query->fetchAll(FetchAs::Associative), fn() => $query->fetchAll(\PDO::FETCH_ASSOC));
   }
 
   /**
@@ -242,7 +244,7 @@ class CssAnalyzerService {
        GROUP BY category",
       [':scan_id' => $scanId]
     );
-    $result = $query->fetchAll(\PDO::FETCH_ASSOC);
+    $result = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $query->fetchAll(FetchAs::Associative), fn() => $query->fetchAll(\PDO::FETCH_ASSOC));
 
     $totals = array_sum(array_column($result, 'total_size')) ?: 1;
     foreach ($result as &$row) {
@@ -258,7 +260,7 @@ class CssAnalyzerService {
     $query = $this->getDatabase()->query(
       "SELECT stylesheet, COUNT(*) AS rule_count, SUM(rule_size) AS total_size, SUM(CASE WHEN is_used = 0 THEN 1 ELSE 0 END) AS unused_count FROM {css_analyzer_rules} WHERE category = 'component' GROUP BY stylesheet ORDER BY unused_count DESC LIMIT 50"
     );
-    return $query->fetchAll(\PDO::FETCH_ASSOC);
+    return DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $query->fetchAll(FetchAs::Associative), fn() => $query->fetchAll(\PDO::FETCH_ASSOC));
   }
 
   /**
