diff --git a/.babel.cfg b/.babel.cfg
new file mode 100644
index 00000000..cc4c9de0
--- /dev/null
+++ b/.babel.cfg
@@ -0,0 +1,3 @@
+[javascript: **.js]
+
+[jinja2: **.html]
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..8452ef07
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: monthly
+    assignees:
+      - "ezio-melotti"
+    groups:
+      actions:
+        patterns:
+          - "*"
diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 00000000..9d1e0987
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,5 @@
+changelog:
+  exclude:
+    authors:
+      - dependabot
+      - pre-commit-ci
diff --git a/.github/workflows/documentation-links.yml b/.github/workflows/documentation-links.yml
new file mode 100644
index 00000000..a4de7dcd
--- /dev/null
+++ b/.github/workflows/documentation-links.yml
@@ -0,0 +1,24 @@
+name: Read the Docs PR preview
+# Automatically edits a pull request's descriptions with a link
+# to the documentation's preview on Read the Docs.
+
+on:
+  pull_request_target:
+    types:
+      - opened
+
+permissions:
+  pull-requests: write
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  documentation-links:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: readthedocs/actions/preview@v1
+        with:
+          project-slug: "python-docs-theme-previews"
+          single-version: "true"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000..88c0c7cb
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,20 @@
+name: Lint
+
+on: [push, pull_request, workflow_dispatch]
+
+env:
+  FORCE_COLOR: 1
+
+permissions:
+  contents: read
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.x"
+      - uses: pre-commit/action@v3.0.1
diff --git a/.github/workflows/pypi-package.yml b/.github/workflows/pypi-package.yml
new file mode 100644
index 00000000..744d49bd
--- /dev/null
+++ b/.github/workflows/pypi-package.yml
@@ -0,0 +1,52 @@
+name: Build & maybe upload PyPI package
+
+on:
+  push:
+  pull_request:
+  release:
+    types:
+      - published
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+env:
+  FORCE_COLOR: 1
+
+jobs:
+  # Always build & lint package.
+  build-package:
+    name: Build & verify package
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: hynek/build-and-inspect-python-package@v2
+
+  # Upload to real PyPI on GitHub Releases.
+  release-pypi:
+    name: Publish to PyPI
+    environment: release-pypi
+    # Only run for published releases.
+    if: |
+      github.repository_owner == 'python'
+      && github.event.action == 'published'
+    runs-on: ubuntu-latest
+    needs: build-package
+
+    permissions:
+      id-token: write
+
+    steps:
+      - name: Download packages built by build-and-inspect-python-package
+        uses: actions/download-artifact@v4
+        with:
+          name: Packages
+          path: dist
+
+      - name: Upload package to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          attestations: true
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 824bb0c6..23e6c7e6 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,36 +1,89 @@
----
-
 name: Tests
 
-on:
-  push:
-    branches:
-      - master
-  pull_request:
-    branches:
-      - master
+on: [push, pull_request, workflow_dispatch]
+
+env:
+  FORCE_COLOR: 1
 
 jobs:
   build_doc:
-    name: 'Build cpython doc'
+    name: Build CPython docs
     runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        branch: ["3.14", "3.13", "3.12"]
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
-          python-version: 3.8
-      - name: clone docsbuild scripts
+          python-version: ${{ matrix.branch }}
+          allow-prereleases: true
+          cache: pip
+      - name: Clone docsbuild scripts
         run: |
           git clone https://github.com/python/docsbuild-scripts/
-      - name: setup requirements
+      - name: Set up requirements
         run: |
           python -m pip install --upgrade pip
-          python -m pip install jinja2 markupsafe zc.lockfile
-      - name: build docs
+          python -m pip install -r docsbuild-scripts/requirements.txt
+      - name: Build documentation
+        run: >
+          python ./docsbuild-scripts/build_docs.py
+          --quick
+          --build-root ./build_root
+          --www-root ./www
+          --log-directory ./logs
+          --group "$(id -g)"
+          --skip-cache-invalidation
+          --theme "$(pwd)"
+          --languages en
+          --branches ${{ matrix.branch }}
+          ${{ matrix.branch == '3.14' && '--select-output no-html' || '' }}
+      - name: Show logs
+        if: failure()
         run: |
-          python3 ./docsbuild-scripts/build_docs.py --quick --build-root ./build_root --www-root ./www --log-directory ./logs --group $(id -g) --skip-cache-invalidation --theme $(pwd) --language en --branch 3.9
-      - name: 'Upload'
-        uses: actions/upload-artifact@v2.2.2
+          cat ./logs/docsbuild.log
+      - name: Upload
+        uses: actions/upload-artifact@v4
         with:
-          name: doc-html
+          name: doc-html-${{ matrix.branch }}
           path: www/
+
+  translations:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: ["ubuntu-latest", "windows-latest"]
+        # Test minimum supported and latest stable from 3.x series
+        python-version: ["3.12", "3"]
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python-version }}
+          allow-prereleases: true
+          cache: pip
+      - name: Install dependencies
+        run: |
+          pip install --upgrade pip
+          pip install -r requirements.txt
+      - name: Remove locale file for testing
+        shell: bash
+        run: rm -rf locales/pt_BR/
+      - run: python babel_runner.py extract
+      - run: python babel_runner.py init -l pt_BR
+      - run: python babel_runner.py update
+      - run: python babel_runner.py update -l pt_BR
+      - run: python babel_runner.py compile
+      - run: python babel_runner.py compile -l pt_BR
+      - name: Print .pot file
+        shell: bash
+        run: cat locales/messages.pot
+      - name: Print .po file
+        shell: bash
+        run: cat locales/pt_BR/LC_MESSAGES/messages.po
+      - name: list files in locales dir
+        shell: bash
+        run: ls -R locales/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..9a7e83b3
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,51 @@
+repos:
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.9.3
+    hooks:
+      - id: ruff
+        args: [--exit-non-zero-on-fix]
+
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 24.10.0
+    hooks:
+      - id: black
+
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v5.0.0
+    hooks:
+      - id: check-case-conflict
+      - id: check-merge-conflict
+      - id: check-toml
+      - id: check-yaml
+      - id: debug-statements
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+
+  - repo: https://github.com/python-jsonschema/check-jsonschema
+    rev: 0.31.0
+    hooks:
+      - id: check-dependabot
+      - id: check-github-workflows
+
+  - repo: https://github.com/rhysd/actionlint
+    rev: v1.7.7
+    hooks:
+      - id: actionlint
+
+  - repo: https://github.com/tox-dev/pyproject-fmt
+    rev: v2.5.0
+    hooks:
+      - id: pyproject-fmt
+
+  - repo: https://github.com/abravalheri/validate-pyproject
+    rev: v0.23
+    hooks:
+      - id: validate-pyproject
+
+  - repo: meta
+    hooks:
+      - id: check-hooks-apply
+      - id: check-useless-excludes
+
+ci:
+  autoupdate_schedule: quarterly
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 00000000..7ac1f464
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,15 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+# Project page: https://readthedocs.org/projects/python-docs-theme-previews/
+
+version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3"
+
+  commands:
+    - git clone --depth=1 https://github.com/python/cpython
+    - make html CPYTHON_PATH=cpython
+    - mv cpython/Doc/build _readthedocs
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 73fb1902..07b2735e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,44 +1,203 @@
-=========
 Changelog
 =========
 
-`2022.1 <https://github.com/python/python-docs-theme/releases/tag/v2022.1>`_
-----------------------------------------------------------------------------------
+`2025.5 <https://github.com/python/python-docs-theme/releases/tag/2025.5>`_
+---------------------------------------------------------------------------
+
+* Make the copy button only appear when hovered over or clicked on by @kevteg in https://github.com/python/python-docs-theme/pull/243
+
+`2025.4.1 <https://github.com/python/python-docs-theme/releases/tag/2025.4.1>`_
+-------------------------------------------------------------------------------
+
+* Fix copy button with multiple tracebacks by @tomasr8 in https://github.com/python/python-docs-theme/pull/240
+
+`2025.4 <https://github.com/python/python-docs-theme/releases/tag/2025.4>`_
+---------------------------------------------------------------------------
+
+* Require Sphinx 7.3 by @AA-Turner in https://github.com/python/python-docs-theme/pull/221
+* Add support for Python 3.14 by @hugovk https://github.com/python/python-docs-theme/pull/236
+* Drop support for Python 3.10 and 3.11 by @hugovk in https://github.com/python/python-docs-theme/pull/234
+* Add a copy button to code samples by @tomasr8 in https://github.com/python/python-docs-theme/pull/231
+* Add missing i18n for copy button titles by @tomasr8 in https://github.com/python/python-docs-theme/pull/225
+* Use consistent line-height for light & dark theme by @tomasr8 in https://github.com/python/python-docs-theme/pull/227
+* Remove self-closing tags by @hugovk in https://github.com/python/python-docs-theme/pull/226
+* Replace deprecated classifier with licence expression (PEP 639) by @hugovk in https://github.com/python/python-docs-theme/pull/237
+
+`2025.2 <https://github.com/python/python-docs-theme/releases/tag/2025.2>`_
+---------------------------------------------------------------------------
+
+- Note minimum requirements for Sphinx (#216)
+    Contributed by Adam Turner
+- Horizontally centre the sidebar collapse button (#219)
+    Contributed by Tomas Roun
+- Make sidebar width more flexible (#218)
+    Contributed by Tomas Roun
+- Set ``__version__`` in the runtime package (#222)
+    Contributed by Adam Turner
+
+`2024.12 <https://github.com/python/python-docs-theme/releases/tag/2024.12>`_
+-----------------------------------------------------------------------------
+
+- Hide header and search bar when printing (#204)
+    Contributed by Hugo van Kemenade
+
+`2024.10 <https://github.com/python/python-docs-theme/releases/tag/2024.10>`_
+-----------------------------------------------------------------------------
+
+- Add support for Python 3.13 (#196)
+    Contributed by Hugo van Kemenade
+- Drop support for Python 3.8 (#197)
+    Contributed by Hugo van Kemenade
+- Add script for handling translations (#195)
+    Contributed by Rafael Fontenelle
+- Generate digital attestations for PyPI (PEP 740) (#198)
+    Contributed by Hugo van Kemenade
+
+`2024.6 <https://github.com/python/python-docs-theme/releases/tag/2024.6>`_
+---------------------------------------------------------------------------
+
+- Add backgrounds and borders to admonitions (#190)
+    Contributed by Hugo van Kemenade
+- Use different colour for 'Return value: Borrowed reference' (#188)
+    Contributed by Hugo van Kemenade
+
+`2024.4 <https://github.com/python/python-docs-theme/releases/tag/2024.4>`_
+---------------------------------------------------------------------------
+
+- Add colour to version change directives (#185)
+    Contributed by Hugo van Kemenade
+- Only show 'Last updated on ...' when ``last_updated`` defined (#183)
+    Contributed by Hugo van Kemenade
+- Use system font stack for all code (#186)
+    Contributed by Hugo van Kemenade
+
+`2024.3 <https://github.com/python/python-docs-theme/releases/tag/2024.3>`_
+---------------------------------------------------------------------------
+
+- Modernise font: use system font stack to improve text readability and webpage performance (#174)
+    Contributed by Hugo van Kemenade
+- Remove incorrect CSS property (#178)
+    Contributed by Kerim Kabirov
+
+`2024.2 <https://github.com/python/python-docs-theme/releases/tag/2024.2>`_
+---------------------------------------------------------------------------
+
+- Do not underline navigation links (#169)
+   Contributed by Hugo van Kemenade
+- Only apply underline offset to code formatting for underline visibility (#171)
+   Contributed by Hugo van Kemenade
+
+`2024.1 <https://github.com/python/python-docs-theme/releases/tag/2024.1>`_
+---------------------------------------------------------------------------
+
+- Underline links for readability and a11y (#160, #166)
+   Contributed by Hugo van Kemenade
+- Add ``hosted_on`` variable for a link in the footer (#165)
+   Contributed by Hugo van Kemenade
+- Consistently reference ``theme_root_icon`` (#163)
+   Contributed by Marko Budiselic
+- Dark mode: fix contrast of footer highlight (#162)
+   Contributed by Hugo van Kemenade
+
+`2023.9 <https://github.com/python/python-docs-theme/releases/tag/2023.9>`_
+---------------------------------------------------------------------------
+
+- Focus search box when pressing slash (#153)
+   Contributed by Hugo van Kemenade
+
+`2023.8 <https://github.com/python/python-docs-theme/releases/tag/2023.8>`_
+---------------------------------------------------------------------------
+
+- Add Python 3.12 and 3.13 classifiers (#147)
+   Contributed by Hugo van Kemenade
+- Dark mode: Also give aside.topic a dark background (#150)
+   Contributed by Hugo van Kemenade
+- Restore the menu on mobile devices (inadvertently broken in 2023.7) (#146)
+   Contributed by Hugo van Kemenade
+
+`2023.7 <https://github.com/python/python-docs-theme/releases/tag/2023.7>`_
+---------------------------------------------------------------------------
+
+- Fix compatibility with Sphinx 7.1 (#137)
+   Contributed by Pradyun Gedam
+- Enable the slash keypress to focus the search field (#141)
+   Contributed by Mike Fiedler
+- Sphinx 6.2 fix: add ``nav.contents`` where ``div.topic`` is used (#138)
+   Contributed by Hugo van Kemenade
+- Dark mode: fix contrast for C++ specific styling (#133)
+   Contributed by Hugo van Kemenade
+- Don't let long code literals extend beyond the right side of the screen (#139)
+   Contributed by Hugo van Kemenade
+- Test with Python 3.12 (#140)
+   Contributed by Hugo van Kemenade
+
+`2023.5 <https://github.com/python/python-docs-theme/releases/tag/2023.5>`_
+---------------------------------------------------------------------------
+
+- Add a dark theme. (#44)
+   Contributed by Nils K
+- Fix: Remove searchbox id from form. (fixes #117)
+   Contributed by Nils K
+- Update ``python-docs-theme`` to work with Sphinx 5 & 6. (#99 & #127)
+   Contributed by Adam Turner
+- Override font for ``.sig`` for consistency with other code blocks. (#121)
+   Contributed by Chris Warrick
+- Dark mode: add class to invert image brightness. (#128)
+   Contributed by Hugo van Kemenade
+
+
+`2023.3.1 <https://github.com/python/python-docs-theme/releases/tag/2023.3.1>`_
+-------------------------------------------------------------------------------
+
+- Skip cache-busting for old Sphinx #113
+
+
+`2023.3 <https://github.com/python/python-docs-theme/releases/tag/2023.3>`_
+---------------------------------------------------------------------------
+
+- Fix problem with monospace rendering in Vivaldi #104
+- Fix mobile nav obstructing content #96
+- Reduce footer margin only for desktop #106
+- Append a hash ?digest to CSS files for cache-busting #108
+
+
+`2022.1 <https://github.com/python/python-docs-theme/releases/tag/2022.1>`_
+----------------------------------------------------------------------------
 
 - Add a configuration for license URL. (#90)
 - Exclude the floating navbar from CHM help. (#84)
 - Make sidebar scrollable and sticky (on modern browsers) (#91)
 
 
-`2021.11.1 <https://github.com/python/python-docs-theme/releases/tag/v2021.11.1>`_
------------------------------------------------------------------------------------
+`2021.11.1 <https://github.com/python/python-docs-theme/releases/tag/2021.11.1>`_
+----------------------------------------------------------------------------------
 
 - Fix monospace again, on buggy Google Chrome (#87)
-  Contributed by Tushar Sadhwani
+   Contributed by Tushar Sadhwani
 
 
-`2021.11 <https://github.com/python/python-docs-theme/releases/tag/v2021.11>`_
--------------------------------------------------------------------------------
+`2021.11 <https://github.com/python/python-docs-theme/releases/tag/2021.11>`_
+------------------------------------------------------------------------------
 
 - Fix monospace on buggy Google Chrome (#85)
-  Contributed by Tushar Sadhwani
+   Contributed by Tushar Sadhwani
 
 
-`2021.8 <https://github.com/python/python-docs-theme/releases/tag/v2021.8>`_
-------------------------------------------------------------------------------
+`2021.8 <https://github.com/python/python-docs-theme/releases/tag/2021.8>`_
+-----------------------------------------------------------------------------
 
 - Add the copyright_url variable in the theme (#67)
-  Contributed by jablonskidev
+   Contributed by jablonskidev
 - Improve readability (#79)
-  Contributed by Olga Bulat
+   Contributed by Olga Bulat
 - Remove #searchbox on mobile to fix a layout bug (#76)
-  Contributed by Olga Bulat
+   Contributed by Olga Bulat
 - Fix the appearance of version/language selects (#74)
-  Contributed by Olga Bulat
+   Contributed by Olga Bulat
 
 
-`2021.5 <https://github.com/python/python-docs-theme/releases/tag/v2021.5>`_
-------------------------------------------------------------------------------
+`2021.5 <https://github.com/python/python-docs-theme/releases/tag/2021.5>`_
+-----------------------------------------------------------------------------
 
 - Make the theme responsive (#46)
    Contributed by Olga Bulat.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 8f5edaa5..c85b77ca 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,15 +1,28 @@
-# How to release
-
-- Update CHANGELOG.rst
-- bump version (YYYY.MM) in setup.py and python_docs_theme/theme.conf
-- commit
-- push to check one last time if the tests pass github side.
-- tag it (YYYY.MM).
-- build (``python -m build``)
-- Test it (in :file:`cpython/Doc` run
-  ``./venv/bin/pip install ../../python-docs-theme/dist/python-docs-theme-2021.8.tar.gz``
-  then build the doc using ``make html``).
-- upload it: ``twine upload dist/*``.
-- bump version (YYYY.MM.dev) in setup.py and python_docs_theme/theme.conf
-- Commit this last bump.
-- push and push the tag (``git push && git push --tags``)
+How to release
+--------------
+
+- Update ``CHANGELOG.rst``
+- Bump version (YYYY.MM) in ``python_docs_theme/__init__.py``
+- Commit
+- Push to check tests pass on
+  `GitHub Actions <https://github.com/python/python-docs-theme/actions>`__
+- Go to https://github.com/python/python-docs-theme/releases
+- Click "Draft a new release"
+- Click "Choose a tag"
+- Type the next YYYY.MM version (no leading zero) and
+  select "**Create new tag: YYYY.MM** on publish"
+- Leave the "Release title" blank (it will be autofilled)
+- Click "Generate release notes" and amend as required
+- Click "Publish release"
+- Check the tagged `GitHub Actions build <https://github.com/python/python-docs-theme/actions/workflows/pypi-package.yml>`__
+  has deployed to `PyPI <https://pypi.org/project/python-docs-theme/#history>`__
+
+Makefile usage
+--------------
+
+This project includes a simple Makefile for syncing changes to the theme with
+the main CPython repository. Run ``make help`` for details on available rules.
+
+There is one configurable variable, ``CPYTHON_PATH``, which should be the path
+to the CPython repository on your machine. By default, it points to
+``../cpython``.
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 3ae74ecc..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,2 +0,0 @@
-include LICENSE README.rst
-recursive-include python_docs_theme *.*
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..bae89325
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,29 @@
+# You can set these variables from the command line.
+CPYTHON_PATH = ../cpython
+PYTHON       = python3
+PACKAGE_ABS_PATH = $(shell pwd)/$(shell find dist/python-docs-theme-*.tar.gz)
+
+
+.PHONY: help
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  venv       to create a venv with necessary tools at $(CPYTHON_PATH)/Doc/venv"
+	@echo "  html       to make standalone CPython HTML files"
+	@echo "  htmlview   to open the index page built by the html target in your browser"
+
+.PHONY: venv
+venv:
+	$(PYTHON) -m pip install build
+	$(PYTHON) -m build
+	cd $(CPYTHON_PATH)/Doc \
+		&& make venv \
+		&& ./venv/bin/pip install $(PACKAGE_ABS_PATH)
+
+.PHONY: html
+html: venv
+	cd $(CPYTHON_PATH)/Doc && \
+		make html
+
+.PHONY: htmlview
+htmlview: html
+	$(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(CPYTHON_PATH)/Doc/build/html/index.html'))"
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..fedd7f76
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+# Python Docs Sphinx Theme
+
+This is the theme for the Python documentation.
+It requires Python 3.12 or newer and Sphinx 7.3 or newer.
+
+Note that when adopting this theme, you're also borrowing an element of the
+trust and credibility established by the CPython core developers over the
+years. That's fine, and you're welcome to do so for other Python community
+projects if you so choose, but please keep in mind that in doing so you're also
+choosing to accept some of the responsibility for maintaining that collective
+trust.
+
+To use the theme, install it into your docs build environment via `pip`
+(preferably in a virtual environment).
+
+
+## Configuration options
+
+To use this theme, add the following to `conf.py`:
+
+- `html_theme = 'python_docs_theme'`
+
+- `html_sidebars`, defaults taken from https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_sidebars
+
+## Preview
+
+See a demo of the CPython docs using this theme:
+
+- https://python-docs-theme-previews.readthedocs.io
+
+The kitchen sink is a showcase of every Sphinx feature:
+
+- https://sphinx-themes.org/sample-sites/python-docs-theme/kitchen-sink/
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 3f2e8443..00000000
--- a/README.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-Python Docs Sphinx Theme
-=========================
-
-This is the theme for the Python documentation.
-
-Note that when adopting this theme, you're also borrowing an element of the
-trust and credibility established by the CPython core developers over the
-years. That's fine, and you're welcome to do so for other Python community
-projects if you so choose, but please keep in mind that in doing so you're also
-choosing to accept some of the responsibility for maintaining that collective
-trust.
-
-To use the theme, install it into your docs build environment via ``pip`` 
-(preferably in a virtual environment).
-
-
-Configuration options
----------------------
-
-To use this theme, add the following to ``conf.py``:
-
-- ``html_theme = 'python_docs_theme'``
-
-- ``html_sidebars``, defaults taken from http://www.sphinx-doc.org/en/stable/config.html#confval-html_sidebars
diff --git a/babel_runner.py b/babel_runner.py
new file mode 100755
index 00000000..0785ae2f
--- /dev/null
+++ b/babel_runner.py
@@ -0,0 +1,128 @@
+#!/usr/bin/venv python3
+"""Script for handling translations with Babel"""
+from __future__ import annotations
+
+import argparse
+import ast
+import subprocess
+import tomllib
+from pathlib import Path
+
+PROJECT_DIR = Path(__file__).resolve().parent
+PYPROJECT_TOML = PROJECT_DIR / "pyproject.toml"
+INIT_PY = PROJECT_DIR / "python_docs_theme" / "__init__.py"
+
+# Global variables used by pybabel below (paths relative to PROJECT_DIR)
+DOMAIN = "messages"
+COPYRIGHT_HOLDER = "Python Software Foundation"
+LOCALES_DIR = "locales"
+POT_FILE = Path(LOCALES_DIR, f"{DOMAIN}.pot")
+SOURCE_DIR = "python_docs_theme"
+MAPPING_FILE = ".babel.cfg"
+
+
+def get_project_info() -> dict:
+    """Retrieve project's info to populate the message catalog template"""
+    pyproject_text = PYPROJECT_TOML.read_text(encoding="utf-8")
+    project_data = tomllib.loads(pyproject_text)["project"]
+
+    # read __version__ from __init__.py
+    for child in ast.parse(INIT_PY.read_bytes()).body:
+        if not isinstance(child, ast.Assign):
+            continue
+        target = child.targets[0]
+        if not isinstance(target, ast.Name) or target.id != "__version__":
+            continue
+        version_node = child.value
+        if not isinstance(version_node, ast.Constant):
+            continue
+        project_data["version"] = version_node.value
+        break
+
+    return project_data
+
+
+def extract_messages() -> None:
+    """Extract messages from all source files into message catalog template"""
+    Path(PROJECT_DIR, LOCALES_DIR).mkdir(parents=True, exist_ok=True)
+    project_data = get_project_info()
+    subprocess.run(
+        [
+            "pybabel",
+            "extract",
+            "-F",
+            MAPPING_FILE,
+            "--copyright-holder",
+            COPYRIGHT_HOLDER,
+            "--project",
+            project_data["name"],
+            "--version",
+            project_data["version"],
+            "--msgid-bugs-address",
+            project_data["urls"]["Issue tracker"],
+            "-o",
+            POT_FILE,
+            SOURCE_DIR,
+        ],
+        cwd=PROJECT_DIR,
+        check=True,
+    )
+
+
+def init_locale(locale: str) -> None:
+    """Initialize a new locale based on existing message catalog template"""
+    pofile = PROJECT_DIR / LOCALES_DIR / locale / "LC_MESSAGES" / f"{DOMAIN}.po"
+    if pofile.exists():
+        print(f"There is already a message catalog for locale {locale}, skipping.")
+        return
+    cmd = ["pybabel", "init", "-i", POT_FILE, "-d", LOCALES_DIR, "-l", locale]
+    subprocess.run(cmd, cwd=PROJECT_DIR, check=True)
+
+
+def update_catalogs(locale: str) -> None:
+    """Update translations from existing message catalogs"""
+    cmd = ["pybabel", "update", "-i", POT_FILE, "-d", LOCALES_DIR]
+    if locale:
+        cmd.extend(["-l", locale])
+    subprocess.run(cmd, cwd=PROJECT_DIR, check=True)
+
+
+def compile_catalogs(locale: str) -> None:
+    """Compile existing message catalogs"""
+    cmd = ["pybabel", "compile", "-d", LOCALES_DIR]
+    if locale:
+        cmd.extend(["-l", locale])
+    subprocess.run(cmd, cwd=PROJECT_DIR, check=True)
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "command",
+        choices=["extract", "init", "update", "compile"],
+        help="command to be executed",
+    )
+    parser.add_argument(
+        "-l",
+        "--locale",
+        default="",
+        help="language code (needed for init, optional for update and compile)",
+    )
+
+    args = parser.parse_args()
+    locale = args.locale
+
+    if args.command == "extract":
+        extract_messages()
+    elif args.command == "init":
+        if not locale:
+            parser.error("init requires passing the --locale option")
+        init_locale(locale)
+    elif args.command == "update":
+        update_catalogs(locale)
+    elif args.command == "compile":
+        compile_catalogs(locale)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/code_of_conduct.rst b/code_of_conduct.rst
deleted file mode 100644
index 28de97ce..00000000
--- a/code_of_conduct.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-Code of Conduct
-===============
-
-Please note that all interactions on
-`Python Software Foundation <https://www.python.org/psf-landing/>`__-supported
-infrastructure is `covered
-<https://www.python.org/psf/records/board/minutes/2014-01-06/#management-of-the-psfs-web-properties>`__
-by the `PSF Code of Conduct <https://www.python.org/psf/codeofconduct/>`__,
-which includes all infrastructure used in the development of Python itself
-(e.g. mailing lists, issue trackers, GitHub, etc.).
-
-In general this means everyone is expected to be open, considerate, and
-respectful of others no matter what their position is within the project.
-
diff --git a/pyproject.toml b/pyproject.toml
index de11f324..a614351d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,73 @@
 [build-system]
+build-backend = "flit_core.buildapi"
 requires = [
-    "setuptools>=42",
-    "wheel"
+  "flit-core>=3.7",
 ]
-build-backend = "setuptools.build_meta"
 
+[project]
+name = "python-docs-theme"
+description = "The Sphinx theme for the CPython docs and related projects"
+readme = "README.md"
+license = "PSF-2.0"
+license-files = [ "LICENSE" ]
+authors = [ { name = "PyPA", email = "distutils-sig@python.org" } ]
+requires-python = ">=3.12"
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Framework :: Sphinx :: Theme",
+  "Intended Audience :: Developers",
+  "Operating System :: OS Independent",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.12",
+  "Programming Language :: Python :: 3.13",
+  "Programming Language :: Python :: 3.14",
+  "Topic :: Documentation",
+  "Topic :: Software Development :: Documentation",
+]
+dynamic = [ "version" ]
+
+dependencies = [
+  "sphinx>=7.3",
+]
+
+urls.Code = "https://github.com/python/python-docs-theme"
+urls.Download = "https://pypi.org/project/python-docs-theme/"
+urls.Homepage = "https://github.com/python/python-docs-theme/"
+urls."Issue tracker" = "https://github.com/python/python-docs-theme/issues"
+entry-points."sphinx.html_themes".python_docs_theme = "python_docs_theme"
+
+[tool.flit.module]
+name = "python_docs_theme"
+
+[tool.flit.sdist]
+include = [ "python_docs_theme/" ]
+
+[tool.ruff]
+fix = true
+
+lint.select = [
+  "C4",     # flake8-comprehensions
+  "E",      # pycodestyle errors
+  "F",      # pyflakes errors
+  "I",      # isort
+  "ISC",    # flake8-implicit-str-concat
+  "LOG",    # flake8-logging
+  "PGH",    # pygrep-hooks
+  "PYI",    # flake8-pyi
+  "RUF022", # unsorted-dunder-all
+  "RUF100", # unused noqa (yesqa)
+  "UP",     # pyupgrade
+  "W",      # pycodestyle warnings
+  "YTT",    # flake8-2020
+]
+lint.ignore = [
+  "E203", # Whitespace before ':'
+  "E221", # Multiple spaces before operator
+  "E226", # Missing whitespace around arithmetic operator
+  "E241", # Multiple spaces after ','
+]
+lint.isort.required-imports = [ "from __future__ import annotations" ]
+
+[tool.pyproject-fmt]
+max_supported_python = "3.14"
diff --git a/python_docs_theme/__init__.py b/python_docs_theme/__init__.py
index 407d62a8..2336dc1e 100644
--- a/python_docs_theme/__init__.py
+++ b/python_docs_theme/__init__.py
@@ -1,12 +1,24 @@
-import os
+from __future__ import annotations
 
+from pathlib import Path
 
-def setup(app):
-    current_dir = os.path.abspath(os.path.dirname(__file__))
-    app.add_html_theme(
-        'python_docs_theme', current_dir)
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+    from sphinx.application import Sphinx
+    from sphinx.util.typing import ExtensionMetadata
+
+__version__ = "2025.5"
+
+THEME_PATH = Path(__file__).resolve().parent
+
+
+def setup(app: Sphinx) -> ExtensionMetadata:
+    app.require_sphinx("7.3")
+
+    app.add_html_theme("python_docs_theme", str(THEME_PATH))
 
     return {
-        'parallel_read_safe': True,
-        'parallel_write_safe': True,
+        "version": __version__,
+        "parallel_read_safe": True,
+        "parallel_write_safe": True,
     }
diff --git a/python_docs_theme/footerdonate.html b/python_docs_theme/footerdonate.html
index 2aef2ac2..010014d6 100644
--- a/python_docs_theme/footerdonate.html
+++ b/python_docs_theme/footerdonate.html
@@ -1,3 +1,3 @@
 {% trans %}The Python Software Foundation is a non-profit corporation.{% endtrans %}
 <a href="https://www.python.org/psf/donations/">{% trans %}Please donate.{% endtrans %}</a>
-<br />
+<br>
diff --git a/python_docs_theme/layout.html b/python_docs_theme/layout.html
index a9a0e6f6..a74517c9 100644
--- a/python_docs_theme/layout.html
+++ b/python_docs_theme/layout.html
@@ -14,7 +14,7 @@ <h3>{{ _('Navigation') }}</h3>
           </li>
           {%- endfor %}
           {%- block rootrellink %}
-          <li><img src="{{ pathto('_static/py.svg', 1) }}" alt="python logo" style="vertical-align: middle; margin-top: -1px"/></li>
+          <li><img src="{{ pathto('_static/' ~ theme_root_icon, 1) }}" alt="{{ theme_root_icon_alt_text }}" style="vertical-align: middle; margin-top: -1px"></li>
           <li><a href="{{theme_root_url}}">{{theme_root_name}}</a>{{ reldelim1 }}</li>
           <li class="switchers">
             <div class="language_switcher_placeholder"></div>
@@ -37,6 +37,7 @@ <h3>{{ _('Navigation') }}</h3>
                     {{ reldelim2 }}
                 </li>
             {%- endif %}
+            <li class="right">{{ themeselector() }}{{ reldelim2 }}</li>
             {% endblock %}
       </ul>
     </div>
@@ -47,25 +48,36 @@ <h3>{{ _('Navigation') }}</h3>
     {%- if builder != "htmlhelp" %}
     <div class="inline-search" role="search">
         <form class="inline-search" action="{{ pathto('search') }}" method="get">
-          <input placeholder="{{ _('Quick search') }}" aria-label="{{ _('Quick search') }}" type="text" name="q" />
-          <input type="submit" value="{{ _('Go') }}" />
-          <input type="hidden" name="check_keywords" value="yes" />
-          <input type="hidden" name="area" value="default" />
+          <input placeholder="{{ _('Quick search') }}" aria-label="{{ _('Quick search') }}" type="search" name="q" id="search-box">
+          <input type="submit" value="{{ _('Go') }}">
         </form>
     </div>
     {%- endif %}
 {%- endmacro %}
 
+{%- macro themeselector() %}
+<label class="theme-selector-label">
+    {{ _('Theme') }}
+    <select class="theme-selector" oninput="activateTheme(this.value)">
+        <option value="auto" selected>{{ _('Auto') }}</option>
+        <option value="light">{{ _('Light') }}</option>
+        <option value="dark">{{ _('Dark') }}</option>
+    </select>
+</label>
+{%- endmacro %}
+
 {% block relbar1 %} {% if builder != 'qthelp' %} {{ relbar() }} {% endif %} {% endblock %}
 {% block relbar2 %} {% if builder != 'qthelp' %} {{ relbar() }} {% endif %} {% endblock %}
 
-
 {%- block extrahead -%}
-    <link rel="shortcut icon" type="image/png" href="{{ pathto('_static/' + theme_root_icon, 1) }}" />
+    <link rel="stylesheet" href="{{ pathto('_static/pydoctheme_dark.css', 1) }}" media="(prefers-color-scheme: dark)" id="pydoctheme_dark_css">
+    <link rel="shortcut icon" type="image/png" href="{{ pathto('_static/' ~ theme_root_icon, 1) }}">
     {%- if builder != "htmlhelp" %}
         {%- if not embedded %}
             <script type="text/javascript" src="{{ pathto('_static/copybutton.js', 1) }}"></script>
             <script type="text/javascript" src="{{ pathto('_static/menu.js', 1) }}"></script>
+            <script type="text/javascript" src="{{ pathto('_static/search-focus.js', 1) }}"></script>
+            <script type="text/javascript" src="{{ pathto('_static/themetoggle.js', 1) }}"></script>
         {%- endif -%}
     {%- endif -%}
     {{ super() }}
@@ -81,33 +93,35 @@ <h3>{{ _('Navigation') }}</h3>
 {%- if builder != 'htmlhelp' %}
 <div class="mobile-nav">
     <input type="checkbox" id="menuToggler" class="toggler__input" aria-controls="navigation"
-           aria-pressed="false" aria-expanded="false" role="button" aria-label="{{ _('Menu')}}" />
-    <label for="menuToggler" class="toggler__label">
-        <span></span>
-    </label>
+           aria-pressed="false" aria-expanded="false" role="button" aria-label="{{ _('Menu')}}">
     <nav class="nav-content" role="navigation">
-         <a href="{{ theme_root_url }}" class="nav-logo">
-             <img src="{{ pathto('_static/py.svg', 1) }}" alt="Logo"/>
-         </a>
-        <div class="version_switcher_placeholder"></div>
-        {%- if pagename != "search" and builder != "singlehtml" %}
-        <form role="search" class="search" action="{{ pathto('search') }}" method="get">
-            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" class="search-icon">
-                <path fill-rule="nonzero"
-                        d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 001.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 00-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 005.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" fill="#444"></path>
-            </svg>
-            <input type="text" name="q" aria-label="{{ _('Quick search') }}"/>
-            <input type="submit" value="{{ _('Go') }}"/>
-        </form>
-        {%- endif %}
+        <label for="menuToggler" class="toggler__label">
+            <span></span>
+        </label>
+        <span class="nav-items-wrapper">
+            <a href="{{ theme_root_url }}" class="nav-logo">
+                <img src="{{ pathto('_static/' ~ theme_root_icon, 1) }}" alt="{{ theme_root_icon_alt_text }}">
+            </a>
+            <span class="version_switcher_placeholder"></span>
+            {%- if pagename != "search" and builder != "singlehtml" %}
+            <form role="search" class="search" action="{{ pathto('search') }}" method="get">
+                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" class="search-icon">
+                    <path fill-rule="nonzero" fill="currentColor" d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 001.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 00-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 005.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path>
+                </svg>
+                <input placeholder="{{ _('Quick search') }}" aria-label="{{ _('Quick search') }}" type="search" name="q">
+                <input type="submit" value="{{ _('Go') }}">
+            </form>
+            {%- endif %}
+        </span>
     </nav>
     <div class="menu-wrapper">
         <nav class="menu" role="navigation" aria-label="main navigation">
             <div class="language_switcher_placeholder"></div>
+            {{ themeselector() }}
             {%- if logo %}
             <p class="logo">
                 <a href="{{ pathto('index') }}">
-                <img class="logo" src="{{ pathto('_static/' + logo, 1)|e }}" alt="Logo"/>
+                <img class="logo" src="{{ pathto('_static/' ~ logo, 1)|e }}" alt="Logo">
                 </a>
             </p>
             {%- endif %}
@@ -122,21 +136,36 @@ <h3>{{ _('Navigation') }}</h3>
 
 {% block footer %}
     <div class="footer">
-    &copy; {% if theme_copyright_url or hasdoc('copyright') %}<a href="{% if theme_copyright_url %}{{ theme_copyright_url }}{% else %}{{ pathto('copyright') }}{% endif %}">{% endif %}{% trans %}Copyright{% endtrans %}{% if theme_copyright_url or hasdoc('copyright') %}</a>{% endif %} {{ copyright|e }}.
-    <br />
+    &copy; {% if theme_copyright_url or hasdoc('copyright') %}
+      <a href="{% if theme_copyright_url %}{{ theme_copyright_url }}{% else %}{{ pathto('copyright') }}{% endif %}">
+    {% endif %}
+    {% trans %}Copyright{% endtrans %}
+    {% if theme_copyright_url or hasdoc('copyright') %}
+      </a>
+    {% endif %} {{ copyright|e }}.
+    <br>
     {% trans %}This page is licensed under the Python Software Foundation License Version 2.{% endtrans %}
-    <br />
+    <br>
     {% trans %}Examples, recipes, and other code in the documentation are additionally licensed under the Zero Clause BSD License.{% endtrans %}
-    <br />
-    {% if theme_license_url %}{% trans license_file=theme_license_url %}See <a href="{{ license_file }}">History and License</a> for more information.{% endtrans %}<br />{% endif %}
-    <br />
+    <br>
+    {% if theme_license_url %}
+      {% trans license_file=theme_license_url %}See <a href="{{ license_file }}">History and License</a> for more information.{% endtrans %}<br>
+    {% endif %}
+    {% if theme_hosted_on %}
+      {% trans hosted_on=theme_hosted_on %}Hosted on {{ hosted_on }}.{% endtrans %}<br>
+    {% endif %}
+    <br>
 
     {% include "footerdonate.html" %}
-    <br />
+    <br>
 
-    {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}
-    {% if theme_issues_url %}{% trans %}<a href="{{ theme_issues_url }}">Found a bug</a>?{% endtrans %}{% endif %}
-    <br />
+    {%- if last_updated %}
+      {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}
+    {%- endif %}
+    {% if theme_issues_url %}
+      {% trans %}<a href="{{ theme_issues_url }}">Found a bug</a>?{% endtrans %}
+    {% endif %}
+    <br>
 
     {% trans sphinx_version=sphinx_version|e %}Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %}
     </div>
diff --git a/python_docs_theme/static/caret-down.svg b/python_docs_theme/static/caret-down.svg
deleted file mode 100644
index fc55b349..00000000
--- a/python_docs_theme/static/caret-down.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M7.45896 11.25H22.5351c1.043 0 1.5645 1.2598.8262 1.998l-7.5352 7.5411c-.457.457-1.2011.457-1.6582 0L6.63279 13.248c-.73828-.7382-.2168-1.998.82617-1.998z" fill="#444"/></svg>
diff --git a/python_docs_theme/static/copybutton.js b/python_docs_theme/static/copybutton.js
index 16dee005..de071f48 100644
--- a/python_docs_theme/static/copybutton.js
+++ b/python_docs_theme/static/copybutton.js
@@ -1,64 +1,84 @@
-$(document).ready(function() {
-    /* Add a [>>>] button on the top-right corner of code samples to hide
-     * the >>> and ... prompts and the output and thus make the code
-     * copyable. */
-    var div = $('.highlight-python .highlight,' +
-                '.highlight-python3 .highlight,' +
-                '.highlight-pycon .highlight,' +
-                '.highlight-pycon3 .highlight,' +
-                '.highlight-default .highlight');
-    var pre = div.find('pre');
+// Extract copyable text from the code block ignoring the
+// prompts and output.
+function getCopyableText(rootElement) {
+    rootElement = rootElement.cloneNode(true)
+    // tracebacks (.gt) contain bare text elements that
+    // need to be removed
+    const tracebacks = rootElement.querySelectorAll(".gt")
+    for (const el of tracebacks) {
+        while (
+            el.nextSibling &&
+            (el.nextSibling.nodeType !== Node.ELEMENT_NODE ||
+                !el.nextSibling.matches(".gp, .go"))
+        ) {
+            el.nextSibling.remove()
+        }
+    }
+    // Remove all elements with the "go" (Generic.Output),
+    // "gp" (Generic.Prompt), or "gt" (Generic.Traceback) CSS class
+    const elements = rootElement.querySelectorAll(".gp, .go, .gt")
+    for (const el of elements) {
+        el.remove()
+    }
+    return rootElement.innerText.trim()
+}
+
+const loadCopyButton = () => {
+    const button = document.createElement("button")
+    button.classList.add("copybutton")
+    button.type = "button"
+    button.innerText = _("Copy")
+    button.title = _("Copy to clipboard")
+
+    const makeOnButtonClick = () => {
+        let timeout = null
+        // define the behavior of the button when it's clicked
+        return async event => {
+            // check if the clipboard is available
+            if (!navigator.clipboard || !navigator.clipboard.writeText) {
+                return;
+            }
+
+            clearTimeout(timeout)
+            const buttonEl = event.currentTarget
+            const codeEl = buttonEl.nextElementSibling
 
-    // get the styles from the current theme
-    pre.parent().parent().css('position', 'relative');
-    var hide_text = 'Hide the prompts and output';
-    var show_text = 'Show the prompts and output';
-    var border_width = pre.css('border-top-width');
-    var border_style = pre.css('border-top-style');
-    var border_color = pre.css('border-top-color');
-    var button_styles = {
-        'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0',
-        'border-color': border_color, 'border-style': border_style,
-        'border-width': border_width, 'color': border_color, 'text-size': '75%',
-        'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em',
-        'border-radius': '0 3px 0 0'
+            try {
+                await navigator.clipboard.writeText(getCopyableText(codeEl))
+            } catch (e) {
+                console.error(e.message)
+                return
+            }
+
+            buttonEl.innerText = _("Copied!")
+            timeout = setTimeout(() => {
+                buttonEl.innerText = _("Copy")
+            }, 1500)
+        }
     }
 
+    const highlightedElements = document.querySelectorAll(
+        ".highlight-python .highlight,"
+        + ".highlight-python3 .highlight,"
+        + ".highlight-pycon .highlight,"
+        + ".highlight-pycon3 .highlight,"
+        + ".highlight-default .highlight"
+    )
+
     // create and add the button to all the code blocks that contain >>>
-    div.each(function(index) {
-        var jthis = $(this);
-        if (jthis.find('.gp').length > 0) {
-            var button = $('<span class="copybutton">&gt;&gt;&gt;</span>');
-            button.css(button_styles)
-            button.attr('title', hide_text);
-            button.data('hidden', 'false');
-            jthis.prepend(button);
-        }
-        // tracebacks (.gt) contain bare text elements that need to be
-        // wrapped in a span to work with .nextUntil() (see later)
-        jthis.find('pre:has(.gt)').contents().filter(function() {
-            return ((this.nodeType == 3) && (this.data.trim().length > 0));
-        }).wrap('<span>');
-    });
+    highlightedElements.forEach(el => {
+        el.style.position = "relative"
 
-    // define the behavior of the button when it's clicked
-    $('.copybutton').click(function(e){
-        e.preventDefault();
-        var button = $(this);
-        if (button.data('hidden') === 'false') {
-            // hide the code output
-            button.parent().find('.go, .gp, .gt').hide();
-            button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden');
-            button.css('text-decoration', 'line-through');
-            button.attr('title', show_text);
-            button.data('hidden', 'true');
-        } else {
-            // show the code output
-            button.parent().find('.go, .gp, .gt').show();
-            button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible');
-            button.css('text-decoration', 'none');
-            button.attr('title', hide_text);
-            button.data('hidden', 'false');
-        }
-    });
-});
+        // if we find a console prompt (.gp), prepend the (deeply cloned) button
+        const clonedButton = button.cloneNode(true)
+        // the onclick attribute is not cloned, set it on the new element
+        clonedButton.onclick = makeOnButtonClick()
+        el.prepend(clonedButton)
+    })
+}
+
+if (document.readyState !== "loading") {
+    loadCopyButton()
+} else {
+    document.addEventListener("DOMContentLoaded", loadCopyButton)
+}
diff --git a/python_docs_theme/static/menu.js b/python_docs_theme/static/menu.js
index b2eabb3a..c7ab03ea 100644
--- a/python_docs_theme/static/menu.js
+++ b/python_docs_theme/static/menu.js
@@ -1,55 +1,57 @@
-document.addEventListener('DOMContentLoaded', function () {
+document.addEventListener("DOMContentLoaded", function () {
 
     // Make tables responsive by wrapping them in a div and making them scrollable
-    const tables = document.querySelectorAll('table.docutils');
+    const tables = document.querySelectorAll("table.docutils")
     tables.forEach(function(table){
-        table.outerHTML = '<div class="responsive-table__container">' + table.outerHTML + '</div>'
-    });
+        table.outerHTML = '<div class="responsive-table__container">' + table.outerHTML + "</div>"
+    })
 
-    const togglerInput = document.querySelector('.toggler__input');
-    const togglerLabel = document.querySelector('.toggler__label');
-    const sideMenu = document.querySelector('.menu-wrapper');
-    const menuItems = document.querySelectorAll('.menu')
-    const doc = document.querySelector('.document');
-    const body = document.querySelector('body');
+    const togglerInput = document.querySelector(".toggler__input")
+    const togglerLabel = document.querySelector(".toggler__label")
+    const sideMenu = document.querySelector(".menu-wrapper")
+    const menuItems = document.querySelectorAll(".menu")
+    const doc = document.querySelector(".document")
+    const body = document.querySelector("body")
 
     function closeMenu() {
-        togglerInput.checked = false;
-        sideMenu.setAttribute("aria-expanded", 'false');
-        sideMenu.setAttribute('aria-hidden', 'true');
-        togglerLabel.setAttribute('aria-pressed', 'false');
-        body.style.overflow = 'visible';
+        togglerInput.checked = false
+        sideMenu.setAttribute("aria-expanded", "false")
+        sideMenu.setAttribute("aria-hidden", "true")
+        togglerLabel.setAttribute("aria-pressed", "false")
+        body.style.overflow = "visible"
     }
     function openMenu() {
-        togglerInput.checked = true;
-        sideMenu.setAttribute("aria-expanded", 'true');
-        sideMenu.setAttribute('aria-hidden', 'false');
-        togglerLabel.setAttribute('aria-pressed', 'true');
-        body.style.overflow = 'hidden';
+        togglerInput.checked = true
+        sideMenu.setAttribute("aria-expanded", "true")
+        sideMenu.setAttribute("aria-hidden", "false")
+        togglerLabel.setAttribute("aria-pressed", "true")
+        body.style.overflow = "hidden"
     }
 
     // Close menu when link on the sideMenu is clicked
-    sideMenu.addEventListener('click', function (event) {
-        let target = event.target;
-        if (target.tagName.toLowerCase() !== 'a') return;
-        closeMenu();
+    sideMenu.addEventListener("click", function (event) {
+        let target = event.target
+        if (target.tagName.toLowerCase() !== "a") {
+            return
+        }
+        closeMenu()
     })
     // Add accessibility data when sideMenu is opened/closed
-    togglerInput.addEventListener('change', function (e) {
-        togglerInput.checked ? openMenu() : closeMenu();
-    });
+    togglerInput.addEventListener("change", function (_event) {
+        togglerInput.checked ? openMenu() : closeMenu()
+    })
     // Make sideMenu links tabbable only when visible
     for(let menuItem of menuItems) {
         if(togglerInput.checked) {
-          menuItem.setAttribute('tabindex', '0');
+          menuItem.setAttribute("tabindex", "0")
         } else {
-          menuItem.setAttribute('tabindex', '-1');
+          menuItem.setAttribute("tabindex", "-1")
         }
     }
     // Close sideMenu when document body is clicked
-    doc.addEventListener('click', function () {
+    doc.addEventListener("click", function () {
         if (togglerInput.checked) {
-            closeMenu();
+            closeMenu()
         }
     })
-})
\ No newline at end of file
+})
diff --git a/python_docs_theme/static/pydoctheme.css b/python_docs_theme/static/pydoctheme.css
index 151f06ef..48731163 100644
--- a/python_docs_theme/static/pydoctheme.css
+++ b/python_docs_theme/static/pydoctheme.css
@@ -1,7 +1,79 @@
-@import url("default.css");
+/* Common colours */
+:root {
+    --good-color: rgb(41 100 51);
+    --good-border: rgb(79 196 100);
+    --middle-color: rgb(133 72 38);
+    --middle-border: rgb(244, 227, 76);
+    --bad-color: rgb(159 49 51);
+    --bad-border: rgb(244, 76, 78);
+}
+
+/* unset some styles from the classic stylesheet */
+div.document,
+div.body,
+div.related,
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6,
+div.sphinxsidebar a,
+div.sphinxsidebar p,
+div.sphinxsidebar ul,
+div.sphinxsidebar h3,
+div.sphinxsidebar h3 a,
+div.sphinxsidebar h4,
+.menu a,
+.menu p,
+.menu ul,
+.menu h3,
+.menu h3 a,
+.menu h4,
+table.docutils td,
+table.indextable tr.cap,
+pre {
+    background-color: inherit;
+    color: inherit;
+}
+
+/* Add underlines to links */
+a[href] {
+    text-decoration: underline 1px;
+}
+
+/* Increase the underline offset for code to avoid obscuring underscores */
+a[href]:has(> code) {
+    text-underline-offset: 0.25em;
+}
+
+/* No underline for navigation */
+a.headerlink,
+div.genindex-jumpbox a,
+div.modindex-jumpbox a,
+div#search-results a,
+div.sphinxsidebar a,
+div.toctree-wrapper a,
+div[role=navigation] a,
+table.contentstable a,
+table.indextable a {
+    text-decoration: none;
+}
+
+/* Except when hovered */
+div.genindex-jumpbox a:hover,
+div.modindex-jumpbox a:hover,
+div#search-results a:hover,
+div.sphinxsidebar a:hover,
+div.toctree-wrapper a:hover,
+div[role=navigation] a:hover,
+table.contentstable a:hover,
+table.indextable a:hover {
+    text-decoration: underline;
+    text-underline-offset: auto;
+}
 
 body {
-    background-color: white;
     margin-left: 1em;
     margin-right: 1em;
 }
@@ -12,19 +84,23 @@ body {
 }
 
 div.related {
+    margin-top: 0.5em;
     margin-bottom: 1.2em;
     padding: 0.5em 0;
-    border-bottom: 1px solid #ccc;
-    margin-top: 0.5em;
+    border-width: 1px;
+    border-color: #ccc;
 }
 
-div.related a:hover {
-    color: #0095C4;
+.mobile-nav + div.related {
+    border-bottom-style: solid;
+}
+
+.document + div.related {
+    border-top-style: solid;
 }
 
-div.related ~ div.related {
-    border-top: 1px solid #ccc;
-    border-bottom: none;
+div.related a:hover {
+    color: #0095c4;
 }
 
 .related .switchers {
@@ -35,46 +111,62 @@ div.related ~ div.related {
     margin-right: 5px;
 }
 
-.version_switcher_placeholder,
-.language_switcher_placeholder {
-    padding-left: 5px;
-    background-color: white;
+div.related ul::after {
+    content: '';
+    clear: both;
+    display: block;
 }
 
-.inline-search {
-    display: inline;
-}
+.inline-search,
 form.inline-search input {
     display: inline;
 }
-form.inline-search input[type="submit"] {
+
+form.inline-search input[type='submit'] {
     width: 40px;
 }
 
 div.document {
     display: flex;
+    /* Don't let long code literals extend beyond the right side of the screen */
+    overflow-wrap: break-word;
+}
+
+/* Don't let long code literals extend beyond the right side of the screen */
+span.pre {
+    white-space: unset;
 }
 
 div.sphinxsidebar {
+    display: flex;
+    width: min(25vw, 350px);
     float: none;
     position: sticky;
     top: 0;
     max-height: 100vh;
-    background-color: #eeeeee;
+    color: #444;
+    background-color: #eee;
     border-radius: 5px;
     line-height: 130%;
     font-size: smaller;
 }
 
-div.sphinxsidebar h3, div.sphinxsidebar h4 {
+div.sphinxsidebar h3,
+div.sphinxsidebar h4 {
     margin-top: 1.5em;
 }
 
+div.bodywrapper {
+    margin-left: min(25vw, 350px);
+}
+
 div.sphinxsidebarwrapper {
     box-sizing: border-box;
     height: 100%;
     overflow-x: hidden;
     overflow-y: auto;
+    float: none;
+    flex-grow: 1;
 }
 
 div.sphinxsidebarwrapper > h3:first-child {
@@ -86,28 +178,43 @@ div.sphinxsidebarwrapper > ul > li > ul > li {
 }
 
 div.sphinxsidebar a:hover {
-    color: #0095C4;
+    color: #0095c4;
 }
 
 form.inline-search input,
-div.sphinxsidebar input {
-    font-family: 'Lucida Grande',Arial,sans-serif;
+div.sphinxsidebar input,
+div.related input {
+    font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
     border: 1px solid #999999;
     font-size: smaller;
     border-radius: 3px;
 }
 
-div.sphinxsidebar input[type=text] {
+div.sphinxsidebar input[type='text'] {
     max-width: 150px;
 }
 
+#sidebarbutton {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 12px;
+    min-width: 12px;
+    border-radius: 0 5px 5px 0;
+    border-left: none;
+}
+
+#sidebarbutton:hover {
+    background-color: #AAAAAA;
+}
+
 div.body {
     padding: 0 0 0 1.2em;
 }
 
 div.body p, div.body dd, div.body li, div.body blockquote {
     text-align: left;
-    line-height: 1.4;
+    line-height: 1.6;
 }
 div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 {
     margin: 0;
@@ -126,16 +233,82 @@ div.body pre {
     border: 1px solid #ac9;
 }
 
-div.body div.admonition, div.body div.impl-detail {
+/* Admonitions */
+:root {
+    --admonition-background: #eee;
+    --admonition-border: #ccc;
+    --admonition-color: black;
+    --attention-background: #bbddff5c;
+    --attention-border: #0000ff36;
+    --caution-background: #ffc;
+    --caution-border: #dd6;
+    --danger-background: #ffe4e4;
+    --danger-border: red;
+    --error-background: #ffe4e4;
+    --error-border: red;
+    --hint-background: #dfd;
+    --hint-border: green;
+    --seealso-background: #ffc;
+    --seealso-border: #dd6;
+    --tip-background: #dfd;
+    --tip-border: green;
+    --warning-background: #ffe4e4;
+    --warning-border: red;
+}
+
+div.body div.admonition {
+    background-color: var(--admonition-background);
+    border: 1px solid var(--admonition-border);
     border-radius: 3px;
+    color: var(--admonition-color);
 }
 
-div.body div.impl-detail > p {
-    margin: 0;
+div.body div.admonition.attention {
+    background-color: var(--attention-background);
+    border-color: var(--attention-border);
+}
+
+div.body div.admonition.caution {
+    background-color: var(--caution-background);
+    border-color: var(--caution-border);
+}
+
+div.body div.admonition.danger {
+    background-color: var(--danger-background);
+    border-color: var(--danger-border);
+}
+
+div.body div.admonition.error {
+    background-color: var(--error-background);
+    border-color: var(--error-border);
+}
+
+div.body div.admonition.hint {
+    background-color: var(--hint-background);
+    border-color: var(--hint-border);
+}
+
+div.body div.admonition.seealso {
+    background-color: var(--seealso-background);
+    border-color: var(--seealso-border);
+}
+
+div.body div.admonition.tip {
+    background-color: var(--tip-background);
+    border-color: var(--tip-border);
+}
+
+div.body div.admonition.warning {
+    background-color: var(--warning-background);
+    border-color: var(--warning-border);
+}
+
+div.body div.impl-detail {
+    border-radius: 3px;
 }
 
-div.body div.seealso {
-    border: 1px solid #dddd66;
+div.body div.impl-detail > p {
+    margin: 0;
 }
 
 div.body a {
@@ -147,23 +320,32 @@ div.body a:visited {
 }
 
 div.body a:hover {
-    color: #00B0E4;
+    color: #00b0e4;
 }
 
 tt, code, pre {
-    font-family: "monospace", monospace;
+    font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
     font-size: 96.5%;
 }
 
-div.body tt, div.body code {
+div.body pre {
+    line-height: 120%;
+}
+
+div.body tt,
+div.body code {
     border-radius: 3px;
 }
 
-div.body tt.descname, div.body code.descname {
+div.body tt.descname,
+div.body code.descname {
     font-size: 120%;
 }
 
-div.body tt.xref, div.body a tt, div.body code.xref, div.body a code {
+div.body tt.xref,
+div.body a tt,
+div.body code.xref,
+div.body a code {
     font-weight: normal;
 }
 
@@ -175,61 +357,119 @@ table.docutils {
     margin-bottom: 10px;
 }
 
-table.docutils td, table.docutils th {
+table.docutils td,
+table.docutils th {
     border: 1px solid #ddd !important;
     border-radius: 3px;
+    padding: 0.3em 0.5em;
 }
 
-table p, table li {
+table p,
+table li {
     text-align: left !important;
 }
 
 table.docutils th {
     background-color: #eee;
-    padding: 0.3em 0.5em;
-}
-
-table.docutils td {
-    background-color: white;
-    padding: 0.3em 0.5em;
 }
 
-table.footnote, table.footnote td {
+table.footnote,
+table.footnote td {
     border: 0 !important;
 }
 
 div.footer {
     line-height: 150%;
-    margin-top: -2em;
     text-align: right;
     width: auto;
     margin-right: 10px;
 }
 
+div.footer a {
+    text-underline-offset: auto;
+}
+
 div.footer a:hover {
-    color: #0095C4;
+    color: #0095c4;
+}
+
+/* C API return value annotations */
+:root {
+    --refcount: var(--good-color);
+    --refcount-return-borrowed-ref: var(--middle-color);
 }
 
 .refcount {
-    color: #060;
+    color: var(--refcount);
 }
 
-.stableabi {
-    color: #229;
+.refcount.return_borrowed_ref {
+    color: var(--refcount-return-borrowed-ref)
 }
 
-.highlight {
-    background: none !important;
+.stableabi {
+    color: #229;
 }
 
-dl > dt span ~ em {
-    font-family: "monospace", monospace;
+dl > dt span ~ em,
+.sig {
+    font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
 }
 
 .toctree-wrapper ul {
     padding-left: 20px;
 }
 
+.theme-selector {
+    margin-left: .5em;
+}
+
+div.genindex-jumpbox,
+div.genindex-jumpbox > p {
+    display: inline-flex;
+    flex-wrap: wrap;
+}
+
+div.genindex-jumpbox a {
+    margin: 0 5px;
+    min-width: 30px;
+    text-align: center;
+}
+
+.copybutton {
+    cursor: pointer;
+    position: absolute;
+    top: 0;
+    right: 0;
+    font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
+    font-size: 80%;
+    padding-left: .5em;
+    padding-right: .5em;
+    height: 100%;
+    max-height: min(100%, 2.4em);
+    border-radius: 0 3px 0 0;
+    color: #000;
+    background-color: #fff;
+    border: 1px solid #ac9; /* follows div.body pre */
+    display: none;
+}
+
+.copybutton:hover {
+    background-color: #eee;
+}
+
+.copybutton:active {
+    background-color: #ddd;
+}
+
+.highlight:active .copybutton {
+    display: block;
+}
+
+.highlight:hover .copybutton {
+    display: block;
+}
+
 @media (max-width: 1023px) {
     /* Body layout */
     div.body {
@@ -259,13 +499,16 @@ dl > dt span ~ em {
         margin-inline-end: 0;
     }
     /* Remove sidebar and top related bar */
-    div.related, .sphinxsidebar {
+    div.related, div.sphinxsidebar {
         display: none;
     }
     /* Anchorlinks are not hidden by fixed-positioned navbar when scrolled to */
     html {
         scroll-padding-top: 40px;
     }
+    body {
+        margin-top: 40px;
+    }
 
     /* Top navigation bar */
     .mobile-nav {
@@ -275,7 +518,6 @@ dl > dt span ~ em {
         position: fixed;
         top: 0;
         left: 0;
-        background-color: white;
         box-shadow: rgba(0, 0, 0, 0.25) 0 0 2px 0;
         z-index: 1;
     }
@@ -284,111 +526,79 @@ dl > dt span ~ em {
     }
     .nav-content {
         position: absolute;
-        z-index: 2;
-        left: 0;
-        top: 0;
+        z-index: 1;
         height: 40px;
         width: 100%;
-        max-width: 100vw;
-        padding: 0 1rem 0 45px;
         display: flex;
-        align-items: center;
         background-color: white;
     }
-    .nav-logo {
-        margin-right: 0.7rem;
+    .nav-items-wrapper {
         display: flex;
-        flex: 0 0 auto;
+        flex: auto;
+        padding: .25rem;
+        align-items: stretch;
+    }
+    .nav-logo {
+        margin-right: 1rem;
+        flex-shrink: 0;
+        align-self: center;
     }
     .nav-content img {
+        display: block;
         width: 20px;
-        height: auto;
     }
     .version_switcher_placeholder {
-        flex: 0 1 0;
         margin-right: 1rem;
     }
+    .version_switcher_placeholder > select {
+        height: 100%;
+    }
     .nav-content .search {
         display: flex;
-        flex: 1 1 auto;
-        align-items: center;
-        padding: 0 0 0 2px;
+        flex: auto;
         border: 1px solid #a9a9a9;
-        height: 30px;
-        overflow: hidden;
+        align-items: stretch;
     }
-    .nav-content .search:hover {
-        box-shadow: 0 1px 6px 0 rgba(32,33,36,0.28);
-        border-color: rgba(223,225,229,0);
-    }
-    .nav-content .search input[type=text] {
+    .nav-content .search input[type=search] {
         border: 0;
-        outline: 0;
-        box-shadow: none;
-        width: 40px;
-        height: 28px;
-        flex: 1 1 auto;
+        padding-left: 24px;
+        width: 100%;
+        flex: 1;
     }
     .nav-content .search input[type=submit] {
         height: 100%;
-        appearance: none;
-        -webkit-appearance: none;
-        border: 1px solid transparent;
-        border-left-color: #a9a9a9;
         box-shadow: none;
+        border: 0;
+        border-left: 1px solid #a9a9a9;
         cursor: pointer;
-        background-color: #f0f0f0;
         margin-right: 0;
     }
-    .nav-content .search input[type=submit]:hover {
-        border-color: #a9a9a9;
-    }
     .nav-content .search svg {
-        flex: 0 0 20px;
-        fill: #333;
+        position: absolute;
+        align-self: center;
+        padding-left: 4px;
     }
     .toggler__input {
-        width: 40px;
-        height: 40px;
-        left: 0;
-        opacity: 0;
-        position: absolute;
-        z-index: 3;
-        margin: 0;
+        display: none;
     }
     .toggler__label {
         width: 40px;
-        height: 40px;
-        margin: 0;
-        position: absolute;
         cursor: pointer;
-        top: 0;
-        left: 0;
-        background-color: transparent;
-        border: 1px solid white;
-        box-shadow: none;
-        z-index: 3;
         display: flex;
         align-items: center;
         justify-content: center;
-        padding: 0 8px;
-    }
-    .toggler__label:focus {
-        background-color: #eee;
-        border: 1px solid #ededed;
-        box-shadow: rgba(0, 0, 0, 0.25) 1px 0 2px 0;
+        padding: 8px;
+        flex-shrink: 0;
     }
-    .toggler__label:hover {
-        background-color: #eee;
-        border: 1px solid #ededed;
-        box-shadow: rgba(0, 0, 0, 0.25) 1px 0 2px 0;
+    .toggler__label:hover, .toggler__label:focus {
+        background-color: rgba(127 127 127 / 50%);
     }
     .toggler__label > span {
         position: relative;
         flex: none;
         height: 2px;
         width: 100%;
-        background: #444;
+        background: currentColor;
         transition: all 400ms ease;
     }
     .toggler__label > span::before,
@@ -404,17 +614,17 @@ dl > dt span ~ em {
     .toggler__label > span::after {
         top: 8px;
     }
-    .toggler__input:checked ~ .toggler__label span {
+    .toggler__input:checked ~ nav > .toggler__label span {
         transform: rotate(135deg);
     }
-    .toggler__input:checked ~ .toggler__label span::before {
+    .toggler__input:checked ~ nav > .toggler__label span::before {
         transform: rotate(90deg);
     }
-    .toggler__input:checked ~ .toggler__label span::before,
-    .toggler__input:checked ~ .toggler__label span::after {
+    .toggler__input:checked ~ nav > .toggler__label span::before,
+    .toggler__input:checked ~ nav > .toggler__label span::after {
         top: 0;
     }
-    .toggler__input:checked:hover ~ .toggler__label span {
+    .toggler__input:checked:hover ~ nav > .toggler__label span {
         transform: rotate(315deg);
     }
     .toggler__input:checked ~ .menu-wrapper {
@@ -432,6 +642,7 @@ dl > dt span ~ em {
         width: 300px;
         height: 100%;
         background-color: #eee;
+        color: #444444;
         box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
         overflow-y: auto;
     }
@@ -451,7 +662,6 @@ dl > dt span ~ em {
         font-size: 1.3em;
     }
     .menu-wrapper h3 {
-        color: #444444;
         font-size: 1.4em;
     }
     .menu-wrapper h3 + p,
@@ -460,7 +670,6 @@ dl > dt span ~ em {
     }
     .menu a {
         font-size: smaller;
-        color: #444444;
         text-decoration: none;
     }
     .menu ul {
@@ -476,43 +685,13 @@ dl > dt span ~ em {
     .menu ul li {
         margin-bottom: 0.5rem;
     }
-    .language_switcher_placeholder,
-    .version_switcher_placeholder {
-        position: relative;
-        border: 1px solid #a8a8a8;
-        height: 30px;
-        padding-right: 7px;
-    }
     .language_switcher_placeholder {
         margin-top: 2rem;
     }
-    .language_switcher_placeholder::after,
-    .version_switcher_placeholder::after {
-        content: url('../_static/caret-down.svg');
-        position: absolute;
-        top: 7px;
-        width: 15px;
-        height: 15px;
-        right: 0;
-        pointer-events: none;
-    }
-    .language_switcher_placeholder select,
-    .version_switcher_placeholder select {
-        -webkit-appearance: none;
-        appearance: none;
-        border: 0;
-        height: 100%;
-        background-color: white;
-    }
-    .language_switcher_placeholder:focus-visible,
-    .version_switcher_placeholder:focus-visible {
-        outline-offset: 5px;
-    }
     .language_switcher_placeholder select {
         width: 100%;
     }
     .document {
-        padding-top: 40px;
         position: relative;
         z-index: 0;
     }
@@ -521,4 +700,74 @@ dl > dt span ~ em {
         width: 100%;
         overflow-x: auto;
     }
+
+    .menu .theme-selector-label {
+        margin-top: .5em;
+        display: flex;
+        width: 100%;
+    }
+
+    .menu .theme-selector {
+        flex: auto;
+    }
+}
+
+@media (min-width: 1024px) {
+    div.footer {
+        margin-top: -2em;
+    }
+}
+
+/* Version change directives */
+:root {
+    --versionadded: var(--good-color);
+    --versionchanged: var(--middle-color);
+    --deprecated: var(--bad-color);
+
+    --versionadded-border: var(--good-border);
+    --versionchanged-border: var(--middle-border);
+    --deprecated-border: var(--bad-border);
+}
+
+div.versionadded,
+div.versionchanged,
+div.deprecated,
+div.deprecated-removed {
+    border-left: 3px solid;
+    padding: 0 1rem;
+}
+
+div.versionadded {
+    border-left-color: var(--versionadded-border);
+}
+
+div.versionchanged {
+    border-left-color: var(--versionchanged-border);
+}
+
+div.deprecated,
+div.deprecated-removed,
+div.versionremoved {
+    border-left-color: var(--deprecated-border);
+}
+
+div.versionadded .versionmodified {
+    color: var(--versionadded);
+}
+
+div.versionchanged .versionmodified {
+    color: var(--versionchanged);
+}
+
+div.deprecated .versionmodified,
+div.deprecated-removed .versionmodified,
+div.versionremoved .versionmodified {
+    color: var(--deprecated);
+}
+
+/* Hide header when printing */
+@media print {
+    div.mobile-nav {
+        display: none;
+    }
 }
diff --git a/python_docs_theme/static/pydoctheme_dark.css b/python_docs_theme/static/pydoctheme_dark.css
new file mode 100644
index 00000000..582e4ddb
--- /dev/null
+++ b/python_docs_theme/static/pydoctheme_dark.css
@@ -0,0 +1,191 @@
+/* Common colours */
+:root {
+    --good-color: rgb(79 196 100);
+    --good-border: var(--good-color);
+    --middle-color: rgb(244, 227, 76);
+    --middle-border: var(--middle-color);
+    --bad-color: rgb(244, 76, 78);
+    --bad-border: var(--bad-color);
+}
+
+
+/* Browser elements */
+:root {
+    scrollbar-color: #616161 transparent;
+    color-scheme: dark;
+}
+
+html,
+body {
+    background-color: #222;
+    color: rgba(255, 255, 255, 0.87);
+}
+
+div.related {
+    color: rgba(255, 255, 255, 0.7); /* classic overwrite */
+    border-color: #424242;
+}
+
+/* SIDEBAR */
+div.sphinxsidebar, .menu-wrapper {
+    background-color: #333;
+    color: inherit;
+}
+
+#sidebarbutton {
+    /* important to overwrite style attribute */
+    background-color: #555 !important;
+    color: inherit !important;
+}
+
+div.sidebar, aside.sidebar {
+    background-color: #424242;
+    border-color: #616161;
+}
+
+/* ANCHORS AND HIGHLIGHTS */
+div.body a {
+    color: #7af;
+}
+
+div.body a:visited {
+    color: #09e;
+}
+
+a.headerlink:hover {
+    background-color: #424242;
+}
+
+div.related a {
+    color: currentColor;
+}
+
+div.footer,
+div.footer a {
+    color: currentColor; /* classic overwrites */
+}
+
+dt:target,
+span.highlighted {
+    background-color: #616161;
+}
+
+.footnote:target {
+    background-color: #2c3e50;
+}
+
+/* Below for most things in text */
+
+dl.field-list > dt {
+    background-color: #434;
+}
+
+table.docutils td,
+table.docutils th {
+    border-color: #616161 !important;
+}
+
+table.docutils th {
+    background-color: #424242;
+}
+
+.stableabi {
+    color: #bbf;
+}
+
+div.body pre {
+    border-color: #616161;
+}
+
+code {
+    background-color: #424242;
+}
+
+div.body div.seealso {
+    background-color: rgba(255, 255, 0, 0.1);
+}
+
+div.warning {
+    background-color: rgba(255, 0, 0, 0.2);
+}
+
+.warning code {
+    background-color: rgba(255, 0, 0, 0.5);
+}
+
+/* Admonitions */
+:root {
+    --admonition-background: #ffffff1a;
+    --admonition-border: currentColor;
+    --admonition-color: #ffffffde;
+    --attention-background: #ffffff1a;
+    --attention-border: currentColor;
+    --caution-background: #ffff001a;
+    --caution-border: #dd6;
+    --danger-background: #f003;
+    --danger-border: #f66;
+    --error-background: #f003;
+    --error-border: #f66;
+    --hint-background: #0044117a;
+    --hint-border: green;
+    --seealso-background: #ffff001a;
+    --seealso-border: #dd6;
+    --tip-background: #0044117a;
+    --tip-border: green;
+    --warning-background: #ff000033;
+    --warning-border: #ff6666;
+}
+
+aside.topic,
+div.topic,
+div.note,
+nav.contents {
+    background-color: rgba(255, 255, 255, 0.1);
+    border-color: currentColor;
+}
+
+.note code {
+    background-color: rgba(255, 255, 255, 0.1);
+}
+
+.mobile-nav {
+    box-shadow: rgba(255, 255, 255, 0.25) 0 0 2px 0;
+}
+
+.nav-content {
+    background-color: black;
+}
+
+img.invert-in-dark-mode {
+    filter: invert(1) hue-rotate(.5turn);
+}
+
+/* -- object description styles --------------------------------------------- */
+
+/* C++ specific styling */
+
+/* Override Sphinx's basic.css to fix colour contrast */
+.sig.c   .k, .sig.c   .kt,
+.sig.cpp .k, .sig.cpp .kt {
+    color: #5283ff;
+}
+
+/* Version change directives */
+:root {
+    --versionadded: var(--good-color);
+    --versionchanged: var(--middle-color);
+    --deprecated: var(--bad-color);
+}
+
+.copybutton {
+    color: #ac9; /* follows div.body pre */
+    background-color: #222222; /* follows body */
+}
+
+.copybutton:hover {
+    background-color: #434343;
+}
+
+.copybutton:active {
+    background-color: #656565;
+}
diff --git a/python_docs_theme/static/search-focus.js b/python_docs_theme/static/search-focus.js
new file mode 100644
index 00000000..aa5b42fd
--- /dev/null
+++ b/python_docs_theme/static/search-focus.js
@@ -0,0 +1,21 @@
+function isInputFocused() {
+  const activeElement = document.activeElement;
+  return (
+    activeElement.tagName === 'INPUT' ||
+    activeElement.tagName === 'TEXTAREA' ||
+    activeElement.isContentEditable
+  );
+}
+
+document.addEventListener('keydown', function(event) {
+  if (event.key === '/') {
+    if (!isInputFocused()) {
+      // Prevent "/" from being entered in the search box
+      event.preventDefault();
+
+      // Set the focus on the search box
+      const searchBox = document.getElementById('search-box');
+      searchBox.focus();
+    }
+  }
+});
diff --git a/python_docs_theme/static/sidebar.js b/python_docs_theme/static/sidebar.js
deleted file mode 100644
index 6b5c694f..00000000
--- a/python_docs_theme/static/sidebar.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * sidebar.js
- * ~~~~~~~~~~
- *
- * This script makes the Sphinx sidebar collapsible. This is a slightly
- * modified version of Sphinx's own sidebar.js.
- *
- * .sphinxsidebar contains .sphinxsidebarwrapper.  This script adds in
- * .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton used to
- * collapse and expand the sidebar.
- *
- * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden and the
- * width of the sidebar and the margin-left of the document are decreased.
- * When the sidebar is expanded the opposite happens.  This script saves a
- * per-browser/per-session cookie used to remember the position of the sidebar
- * among the pages.  Once the browser is closed the cookie is deleted and the
- * position reset to the default (expanded).
- *
- * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
- */
-
-$(function() {
-  // global elements used by the functions.
-  // the 'sidebarbutton' element is defined as global after its
-  // creation, in the add_sidebar_button function
-  var bodywrapper = $('.bodywrapper');
-  var sidebar = $('.sphinxsidebar');
-  var sidebarwrapper = $('.sphinxsidebarwrapper');
-
-  // original margin-left of the bodywrapper and width of the sidebar
-  // with the sidebar expanded
-  var bw_margin_expanded = bodywrapper.css('margin-left');
-  var ssb_width_expanded = sidebar.width();
-
-  // margin-left of the bodywrapper and width of the sidebar
-  // with the sidebar collapsed
-  var bw_margin_collapsed = '.8em';
-  var ssb_width_collapsed = '.8em';
-
-  // colors used by the current theme
-  var dark_color = '#AAAAAA';
-  var light_color = '#CCCCCC';
-
-  function sidebar_is_collapsed() {
-    return sidebarwrapper.is(':not(:visible)');
-  }
-
-  function toggle_sidebar() {
-    if (sidebar_is_collapsed())
-      expand_sidebar();
-    else
-      collapse_sidebar();
-  }
-
-  function collapse_sidebar() {
-    sidebarwrapper.hide();
-    sidebar.css('width', ssb_width_collapsed);
-    bodywrapper.css('margin-left', bw_margin_collapsed);
-    sidebarbutton.css({
-        'margin-left': '0',
-        'border-radius': '5px'
-    });
-    sidebarbutton.find('span').text('»');
-    sidebarbutton.attr('title', _('Expand sidebar'));
-    document.cookie = 'sidebar=collapsed';
-  }
-
-  function expand_sidebar() {
-    bodywrapper.css('margin-left', bw_margin_expanded);
-    sidebar.css('width', ssb_width_expanded);
-    sidebarwrapper.show();
-    sidebarbutton.css({
-        'margin-left': ssb_width_expanded-12,
-        'border-radius': '0 5px 5px 0'
-    });
-    sidebarbutton.find('span').text('«');
-    sidebarbutton.attr('title', _('Collapse sidebar'));
-    document.cookie = 'sidebar=expanded';
-  }
-
-  function add_sidebar_button() {
-    sidebarwrapper.css({
-        'float': 'left',
-        'margin-right': '0',
-        'width': ssb_width_expanded - 13
-    });
-    // create the button
-    sidebar.append(
-      '<div id="sidebarbutton"><span>&laquo;</span></div>'
-    );
-    var sidebarbutton = $('#sidebarbutton');
-    sidebarbutton.find('span').css({
-        'display': 'block',
-        'position': 'fixed',
-        'top': '50%'
-    });
-
-    sidebarbutton.click(toggle_sidebar);
-    sidebarbutton.attr('title', _('Collapse sidebar'));
-    sidebarbutton.css({
-        'border-radius': '0 5px 5px 0',
-        'color': '#444444',
-        'background-color': '#CCCCCC',
-        'font-size': '1.2em',
-        'cursor': 'pointer',
-        'height': '100%',
-        'padding-left': '1px',
-        'margin-left': ssb_width_expanded - 12
-    });
-
-    sidebarbutton.hover(
-      function () {
-          $(this).css('background-color', dark_color);
-      },
-      function () {
-          $(this).css('background-color', light_color);
-      }
-    );
-  }
-
-  function set_position_from_cookie() {
-    if (!document.cookie)
-      return;
-    var items = document.cookie.split(';');
-    for(var k=0; k<items.length; k++) {
-      var key_val = items[k].split('=');
-      var key = key_val[0];
-      if (key == 'sidebar') {
-        var value = key_val[1];
-        if ((value == 'collapsed') && (!sidebar_is_collapsed()))
-          collapse_sidebar();
-        else if ((value == 'expanded') && (sidebar_is_collapsed()))
-          expand_sidebar();
-      }
-    }
-  }
-
-  add_sidebar_button();
-  var sidebarbutton = $('#sidebarbutton');
-  set_position_from_cookie();
-});
diff --git a/python_docs_theme/static/themetoggle.js b/python_docs_theme/static/themetoggle.js
new file mode 100644
index 00000000..f9394c66
--- /dev/null
+++ b/python_docs_theme/static/themetoggle.js
@@ -0,0 +1,24 @@
+const pydocthemeDark = document.getElementById('pydoctheme_dark_css')
+const pygmentsDark = document.getElementById('pygments_dark_css')
+const themeSelectors = document.getElementsByClassName('theme-selector')
+
+function activateTheme(theme) {
+  localStorage.setItem('currentTheme', theme);
+  [...themeSelectors].forEach(e => e.value = theme)
+  switch (theme) {
+    case 'light':
+      pydocthemeDark.media = 'not all'
+      pygmentsDark.media = 'not all'
+      break;
+    case 'dark':
+      pydocthemeDark.media = 'all'
+      pygmentsDark.media = 'all'
+      break;
+    default:
+      // auto
+      pydocthemeDark.media = '(prefers-color-scheme: dark)'
+      pygmentsDark.media = '(prefers-color-scheme: dark)'
+  }
+}
+
+activateTheme(localStorage.getItem('currentTheme') || 'auto')
diff --git a/python_docs_theme/theme.conf b/python_docs_theme/theme.conf
deleted file mode 100644
index 11670d3c..00000000
--- a/python_docs_theme/theme.conf
+++ /dev/null
@@ -1,33 +0,0 @@
-[theme]
-inherit = default
-stylesheet = pydoctheme.css?2022.1
-pygments_style = default
-
-[options]
-bodyfont = 'Lucida Grande', Arial, sans-serif
-headfont = 'Lucida Grande', Arial, sans-serif
-footerbgcolor = white
-footertextcolor = #555555
-relbarbgcolor = white
-relbartextcolor = #666666
-relbarlinkcolor = #444444
-sidebarbgcolor = white
-sidebartextcolor = #444444
-sidebarlinkcolor = #444444
-bgcolor = white
-textcolor = #222222
-linkcolor = #0090c0
-visitedlinkcolor = #00608f
-headtextcolor = #1a1a1a
-headbgcolor = white
-headlinkcolor = #aaaaaa
-codebgcolor = #eeffcc
-codetextcolor = #333333
-
-issues_url =
-license_url =
-root_name = Python
-root_url = https://www.python.org/
-root_icon = py.svg
-root_include_title = True
-copyright_url =
diff --git a/python_docs_theme/theme.toml b/python_docs_theme/theme.toml
new file mode 100644
index 00000000..1d3cdbda
--- /dev/null
+++ b/python_docs_theme/theme.toml
@@ -0,0 +1,39 @@
+[theme]
+inherit = "default"
+stylesheets = [
+    "classic.css",
+    "pydoctheme.css",
+]
+pygments_style = { default = "default", dark = "monokai" }
+
+[options]
+bodyfont = "-apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif"
+headfont = "-apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif"
+footerbgcolor = "white"
+footertextcolor = "#555555"
+relbarbgcolor = "white"
+relbartextcolor = "#666666"
+relbarlinkcolor = "#444444"
+sidebarbgcolor = "white"
+sidebartextcolor = "#444444"
+sidebarlinkcolor = "#444444"
+sidebarbtncolor = "#cccccc"
+bgcolor = "white"
+textcolor = "#222222"
+linkcolor = "#0090c0"
+visitedlinkcolor = "#00608f"
+headtextcolor = "#1a1a1a"
+headbgcolor = "white"
+headlinkcolor = "#aaaaaa"
+codebgcolor = "#eeffcc"
+codetextcolor = "#333333"
+
+hosted_on = ""
+issues_url = ""
+license_url = ""
+root_name = "Python"
+root_url = "https://www.python.org/"
+root_icon = "py.svg"
+root_icon_alt_text = "Python logo"
+root_include_title = "True"
+copyright_url = ""
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..bb631b59
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+# for babel_runner.py
+Babel
+Jinja2
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 2a9acf13..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[bdist_wheel]
-universal = 1
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 578cf1c8..00000000
--- a/setup.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python
-
-import io
-
-from setuptools import setup
-
-# README into long description
-with io.open("README.rst", encoding="utf-8") as readme_file:
-    long_description = readme_file.read()
-
-
-setup(
-    name="python-docs-theme",
-    # Version is date based as year.month[.serial], where serial is used
-    # if multiple releases are needed to address build failures.
-    version="2022.1",
-    description="The Sphinx theme for the CPython docs and related projects",
-    long_description=long_description,
-    author="PyPA",
-    author_email="distutils-sig@python.org",
-    url="https://github.com/python/python-docs-theme/",
-    packages=["python_docs_theme"],
-    include_package_data=True,
-    entry_points={
-        "sphinx.html_themes": [
-            "python_docs_theme = python_docs_theme",
-        ]
-    },
-    classifiers=[
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: Python Software Foundation License",
-        "Operating System :: OS Independent",
-        "Topic :: Documentation",
-        "Topic :: Software Development :: Documentation",
-        "Programming Language :: Python",
-        "Programming Language :: Python :: 3",
-    ],
-)
