diff -pruN 0.7.0-3/.github/workflows/doconfly.yml 0.8.0-1/.github/workflows/doconfly.yml
--- 0.7.0-3/.github/workflows/doconfly.yml	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/.github/workflows/doconfly.yml	2025-03-05 14:35:04.000000000 +0000
@@ -2,10 +2,10 @@ name: doconfly
 on:
   push:
     branches:
-      - master
+      - main
     tags:
       - "*"
- 
+
 jobs:
   doconfly:
     name: doconfly job
diff -pruN 0.7.0-3/.github/workflows/release.yml 0.8.0-1/.github/workflows/release.yml
--- 0.7.0-3/.github/workflows/release.yml	1970-01-01 00:00:00.000000000 +0000
+++ 0.8.0-1/.github/workflows/release.yml	2025-03-05 14:35:04.000000000 +0000
@@ -0,0 +1,42 @@
+name: Release new version
+on:
+  push:
+    tags:
+      - *
+
+jobs:
+  pypi-publish:
+    name: Upload release to PyPI
+    runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/cssselect2
+    permissions:
+      id-token: write
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+      - name: Install requirements
+        run: python -m pip install flit
+      - name: Build packages
+        run: flit build
+      - name: Publish package distributions to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+  add-version:
+    name: Add version to GitHub
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Install requirements
+        run: sudo apt-get install pandoc
+      - name: Generate content
+        run: |
+          pandoc docs/changelog.rst -f rst -t gfm | csplit - /##/ "{1}" -f .part
+          sed -r "s/^([A-Z].*)\:\$/## \1/" .part01 | sed -r "s/^ *//" | sed -rz "s/([^\n])\n([^\n^-])/\1 \2/g" | tail -n +5 > .body
+      - name: Create Release
+        uses: softprops/action-gh-release@v2
+        with:
+          body_path: .body
diff -pruN 0.7.0-3/.github/workflows/tests.yml 0.8.0-1/.github/workflows/tests.yml
--- 0.7.0-3/.github/workflows/tests.yml	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/.github/workflows/tests.yml	2025-03-05 14:35:04.000000000 +0000
@@ -6,15 +6,19 @@ jobs:
     name: ${{ matrix.os }} - ${{ matrix.python-version }}
     runs-on: ${{ matrix.os }}
     strategy:
-      fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.8"]
+        python-version: ['3.13']
+        include:
+          - os: ubuntu-latest
+            python-version: '3.9'
+          - os: ubuntu-latest
+            python-version: 'pypy-3.10'
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
         with:
           submodules: true
-      - uses: actions/setup-python@v2
+      - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
       - name: Upgrade pip and setuptools
@@ -24,6 +28,4 @@ jobs:
       - name: Launch tests
         run: python -m pytest
       - name: Check coding style
-        run: python -m flake8 --exclude tests/css-parsing-tests
-      - name: Check imports order
-        run: python -m isort . --check --diff
+        run: python -m ruff check
diff -pruN 0.7.0-3/README.rst 0.8.0-1/README.rst
--- 0.7.0-3/README.rst	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/README.rst	2025-03-05 14:35:04.000000000 +0000
@@ -3,7 +3,7 @@ documents (HTML, XML, etc.) that can be
 (including cElementTree, lxml, html5lib, etc.)
 
 * Free software: BSD license
-* For Python 3.7+, tested on CPython and PyPy
+* For Python 3.9+, tested on CPython and PyPy
 * Documentation: https://doc.courtbouillon.org/cssselect2
 * Changelog: https://github.com/Kozea/cssselect2/releases
 * Code, issues, tests: https://github.com/Kozea/cssselect2
diff -pruN 0.7.0-3/cssselect2/__init__.py 0.8.0-1/cssselect2/__init__.py
--- 0.7.0-3/cssselect2/__init__.py	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/cssselect2/__init__.py	2025-03-05 14:35:04.000000000 +0000
@@ -6,8 +6,6 @@ documents (HTML, XML, etc.) that can be
 
 """
 
-import operator
-
 from webencodings import ascii_lower
 
 # Classes are imported here to expose them at the top level of the module
@@ -15,7 +13,7 @@ from .compiler import compile_selector_l
 from .parser import SelectorError  # noqa
 from .tree import ElementWrapper  # noqa
 
-VERSION = __version__ = '0.7.0'
+VERSION = __version__ = '0.8.0'
 
 
 class Matcher:
@@ -49,19 +47,17 @@ class Matcher:
             return
 
         entry = (
-            selector.test, selector.specificity, self.order,
-            selector.pseudo_element, payload)
+            selector.test, selector.specificity, self.order, selector.pseudo_element,
+            payload)
         if selector.id is not None:
             self.id_selectors.setdefault(selector.id, []).append(entry)
         elif selector.class_name is not None:
-            self.class_selectors.setdefault(
-                selector.class_name, []).append(entry)
+            self.class_selectors.setdefault(selector.class_name, []).append(entry)
         elif selector.local_name is not None:
             self.lower_local_name_selectors.setdefault(
                 selector.lower_local_name, []).append(entry)
         elif selector.namespace is not None:
-            self.namespace_selectors.setdefault(
-                selector.namespace, []).append(entry)
+            self.namespace_selectors.setdefault(selector.namespace, []).append(entry)
         elif selector.requires_lang_attr:
             self.lang_attr_selectors.append(entry)
         else:
@@ -89,8 +85,7 @@ class Matcher:
         for class_name in element.classes:
             if class_name in self.class_selectors:
                 self.add_relevant_selectors(
-                    element, self.class_selectors[class_name],
-                    relevant_selectors)
+                    element, self.class_selectors[class_name], relevant_selectors)
 
         lower_name = ascii_lower(element.local_name)
         if lower_name in self.lower_local_name_selectors:
@@ -106,18 +101,13 @@ class Matcher:
             self.add_relevant_selectors(
                 element, self.lang_attr_selectors, relevant_selectors)
 
-        self.add_relevant_selectors(
-            element, self.other_selectors, relevant_selectors)
+        self.add_relevant_selectors(element, self.other_selectors, relevant_selectors)
 
-        relevant_selectors.sort(key=SORT_KEY)
+        relevant_selectors.sort()
         return relevant_selectors
 
     @staticmethod
     def add_relevant_selectors(element, selectors, relevant_selectors):
         for test, specificity, order, pseudo, payload in selectors:
             if test(element):
-                relevant_selectors.append(
-                    (specificity, order, pseudo, payload))
-
-
-SORT_KEY = operator.itemgetter(0, 1)
+                relevant_selectors.append((specificity, order, pseudo, payload))
diff -pruN 0.7.0-3/cssselect2/compiler.py 0.8.0-1/cssselect2/compiler.py
--- 0.7.0-3/cssselect2/compiler.py	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/cssselect2/compiler.py	2025-03-05 14:35:04.000000000 +0000
@@ -28,10 +28,7 @@ def compile_selector_list(input, namespa
         A list of opaque :class:`compiler.CompiledSelector` objects.
 
     """
-    return [
-        CompiledSelector(selector)
-        for selector in parser.parse(input, namespaces)
-    ]
+    return [CompiledSelector(selector) for selector in parser.parse(input, namespaces)]
 
 
 class CompiledSelector:
@@ -126,17 +123,18 @@ def _compile_node(selector):
 
     elif isinstance(selector, parser.CompoundSelector):
         sub_expressions = [
-            expr for expr in map(_compile_node, selector.simple_selectors)
+            expr for expr in [
+                _compile_node(selector)
+                for selector in selector.simple_selectors]
             if expr != '1']
         if len(sub_expressions) == 1:
-            test = sub_expressions[0]
+            return sub_expressions[0]
         elif '0' in sub_expressions:
-            test = '0'
+            return '0'
         elif sub_expressions:
-            test = ' and '.join(f'({e})' for e in sub_expressions)
+            return ' and '.join(f'({el})' for el in sub_expressions)
         else:
-            test = '1'  # all([]) == True
-        return test
+            return '1'  # all([]) == True
 
     elif isinstance(selector, parser.NegationSelector):
         sub_expressions = [
@@ -209,44 +207,37 @@ def _compile_node(selector):
                     lower, name = selector.lower_name, selector.name
                     key = f'({lower!r} if el.in_html_document else {name!r})'
             value = selector.value
+            attribute_value = f'el.etree_element.get({key}, "")'
+            if selector.case_sensitive is False:
+                value = value.lower()
+                attribute_value += '.lower()'
             if selector.operator is None:
                 return f'{key} in el.etree_element.attrib'
             elif selector.operator == '=':
-                return f'el.etree_element.get({key}) == {value!r}'
+                return (
+                    f'{key} in el.etree_element.attrib and '
+                    f'{attribute_value} == {value!r}')
             elif selector.operator == '~=':
-                if len(value.split()) != 1 or value.strip() != value:
-                    return '0'
-                else:
-                    return (
-                        f'{value!r} in '
-                        f'split_whitespace(el.etree_element.get({key}, ""))')
+                return (
+                    '0' if len(value.split()) != 1 or value.strip() != value
+                    else f'{value!r} in split_whitespace({attribute_value})')
             elif selector.operator == '|=':
                 return (
-                    f'next(v == {value!r} or '
-                    f'     (v is not None and v.startswith({(value + "-")!r}))'
-                    f'     for v in [el.etree_element.get({key})])')
+                    f'{key} in el.etree_element.attrib and '
+                    f'{attribute_value} == {value!r} or '
+                    f'{attribute_value}.startswith({(value + "-")!r})')
             elif selector.operator == '^=':
                 if value:
-                    return (
-                        f'el.etree_element.get({key}, "")'
-                        f'.startswith({value!r})')
+                    return f'{attribute_value}.startswith({value!r})'
                 else:
                     return '0'
             elif selector.operator == '$=':
-                if value:
-                    return (
-                        f'el.etree_element.get({key}, "")'
-                        f'.endswith({value!r})')
-                else:
-                    return '0'
+                return (
+                    f'{attribute_value}.endswith({value!r})' if value else '0')
             elif selector.operator == '*=':
-                if value:
-                    return f'{value!r} in el.etree_element.get({key}, "")'
-                else:
-                    return '0'
+                return f'{value!r} in {attribute_value}' if value else '0'
             else:
-                raise SelectorError(
-                    'Unknown attribute operator', selector.operator)
+                raise SelectorError('Unknown attribute operator', selector.operator)
         else:  # In any namespace
             raise NotImplementedError  # TODO
 
@@ -404,7 +395,7 @@ def _compile_node(selector):
             # Matches if a positive or zero integer n exists so that:
             # x = a*n + b-1
             # x = a*n + B
-            B = b - 1
+            B = b - 1  # noqa: N806
             if a == 0:
                 # x = B
                 return f'({count}) == {B}'
@@ -421,14 +412,15 @@ def _compile_node(selector):
 def html_tag_eq(*local_names):
     """Generate expression testing equality with HTML local names."""
     if len(local_names) == 1:
-        tag = '{http://www.w3.org/1999/xhtml}' + local_names[0]
+        tag = f'{{http://www.w3.org/1999/xhtml}}{local_names[0]}'
         return (
             f'((el.local_name == {local_names[0]!r}) if el.in_html_document '
             f'else (el.etree_element.tag == {tag!r}))')
     else:
         names = ', '.join(repr(n) for n in local_names)
         tags = ', '.join(
-            repr('{http://www.w3.org/1999/xhtml}' + n) for n in local_names)
+            repr(f'{{http://www.w3.org/1999/xhtml}}{name}')
+            for name in local_names)
         return (
             f'((el.local_name in ({names})) if el.in_html_document '
             f'else (el.etree_element.tag in ({tags})))')
diff -pruN 0.7.0-3/cssselect2/parser.py 0.8.0-1/cssselect2/parser.py
--- 0.7.0-3/cssselect2/parser.py	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/cssselect2/parser.py	2025-03-05 14:35:04.000000000 +0000
@@ -136,8 +136,7 @@ def parse_simple_selector(tokens, namesp
         if next == ':':
             next = tokens.next()
             if next is None or next.type != 'ident':
-                raise SelectorError(
-                    next, f'Expected a pseudo-element name, got {next}')
+                raise SelectorError(next, f'Expected a pseudo-element name, got {next}')
             value = next.lower_value
             if value not in SUPPORTED_PSEUDO_ELEMENTS:
                 raise SelectorError(
@@ -154,8 +153,7 @@ def parse_simple_selector(tokens, namesp
             if name in ('is', 'where', 'not', 'has'):
                 return parse_logical_combination(next, namespaces, name), None
             else:
-                return (
-                    FunctionalPseudoClassSelector(name, next.arguments), None)
+                return (FunctionalPseudoClassSelector(name, next.arguments), None)
         else:
             raise SelectorError(next, f'unexpected {next} token.')
     else:
@@ -185,8 +183,7 @@ def parse_logical_combination(matches_an
 
 def parse_attribute_selector(tokens, namespaces):
     tokens.skip_whitespace()
-    qualified_name = parse_qualified_name(
-        tokens, namespaces, is_attribute=True)
+    qualified_name = parse_qualified_name(tokens, namespaces, is_attribute=True)
     if qualified_name is None:
         next = tokens.next()
         raise SelectorError(next, f'expected attribute name, got {next}')
@@ -204,18 +201,22 @@ def parse_attribute_selector(tokens, nam
         next = tokens.next()
         if next is None or next.type not in ('ident', 'string'):
             next_type = 'None' if next is None else next.type
-            raise SelectorError(
-                next, f'expected attribute value, got {next_type}')
+            raise SelectorError(next, f'expected attribute value, got {next_type}')
         value = next.value
     else:
-        raise SelectorError(
-            peek, f'expected attribute selector operator, got {peek}')
+        raise SelectorError(peek, f'expected attribute selector operator, got {peek}')
 
     tokens.skip_whitespace()
     next = tokens.next()
+    case_sensitive = None
     if next is not None:
-        raise SelectorError(next, f'expected ], got {next.type}')
-    return AttributeSelector(namespace, local_name, operator, value)
+        if next.type == 'ident' and next.value.lower() == 'i':
+            case_sensitive = False
+        elif next.type == 'ident' and next.value.lower() == 's':
+            case_sensitive = True
+        else:
+            raise SelectorError(next, f'expected ], got {next.type}')
+    return AttributeSelector(namespace, local_name, operator, value, case_sensitive)
 
 
 def parse_qualified_name(tokens, namespaces, is_attribute=False):
@@ -239,15 +240,13 @@ def parse_qualified_name(tokens, namespa
         namespace = namespaces.get(first_ident.value)
         if namespace is None:
             raise SelectorError(
-                first_ident,
-                f'undefined namespace prefix: {first_ident.value}')
+                first_ident, f'undefined namespace prefix: {first_ident.value}')
     elif peek == '*':
         next = tokens.next()
         peek = tokens.peek()
         if peek != '|':
             if is_attribute:
-                raise SelectorError(
-                    next, f'expected local name, got {next.type}')
+                raise SelectorError(next, f'expected local name, got {next.type}')
             return namespaces.get(None, None), None
         tokens.next()
         namespace = None
@@ -398,10 +397,7 @@ class NamespaceSelector:
         self.namespace = namespace
 
     def __repr__(self):
-        if self.namespace == '':
-            return '|'
-        else:
-            return f'{{{self.namespace}}}|'
+        return '|' if self.namespace == '' else f'{{{self.namespace}}}|'
 
 
 class IDSelector:
@@ -427,17 +423,25 @@ class ClassSelector:
 class AttributeSelector:
     specificity = 0, 1, 0
 
-    def __init__(self, namespace, name, operator, value):
+    def __init__(self, namespace, name, operator, value, case_sensitive):
         self.namespace = namespace
         self.name, self.lower_name = name
         #: A string like ``=`` or ``~=``, or None for ``[attr]`` selectors
         self.operator = operator
         #: A string, or None for ``[attr]`` selectors
         self.value = value
+        #: ``True`` if case-sensitive, ``False`` if case-insensitive, ``None``
+        #: if depends on the document language
+        self.case_sensitive = case_sensitive
 
     def __repr__(self):
         namespace = '*|' if self.namespace is None else f'{{{self.namespace}}}'
-        return f'[{namespace}{self.name}{self.operator}{self.value!r}]'
+        case_sensitive = (
+            '' if self.case_sensitive is None else
+            f' {"s" if self.case_sensitive else "i"}')
+        return (
+            f'[{namespace}{self.name}{self.operator}{self.value!r}'
+            f'{case_sensitive}]')
 
 
 class PseudoClassSelector:
diff -pruN 0.7.0-3/cssselect2/tree.py 0.8.0-1/cssselect2/tree.py
--- 0.7.0-3/cssselect2/tree.py	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/cssselect2/tree.py	2025-03-05 14:35:04.000000000 +0000
@@ -1,34 +1,10 @@
-import functools
+from functools import cached_property
 from warnings import warn
 
 from webencodings import ascii_lower
 
 from .compiler import compile_selector_list, split_whitespace
 
-if hasattr(functools, 'cached_property'):
-    # Python 3.8+
-    cached_property = functools.cached_property
-else:
-    # Python 3.7
-    class cached_property:
-        # Borrowed from Werkzeug
-        # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/utils.py
-
-        def __init__(self, func, name=None, doc=None):
-            self.__name__ = name or func.__name__
-            self.__module__ = func.__module__
-            self.__doc__ = doc or func.__doc__
-            self.func = func
-
-        def __get__(self, obj, type=None, __missing=object()):
-            if obj is None:
-                return self
-            value = obj.__dict__.get(self.__name__, __missing)
-            if value is __missing:
-                value = self.func(obj)
-                obj.__dict__[self.__name__] = value
-            return value
-
 
 class ElementWrapper:
     """Wrapper of :class:`xml.etree.ElementTree.Element` for Selector matching.
@@ -77,8 +53,7 @@ class ElementWrapper:
             root = root.getroot()
         return cls(
             root, parent=None, index=0, previous=None,
-            in_html_document=in_html_document,
-            content_language=content_language)
+            in_html_document=in_html_document, content_language=content_language)
 
     def __init__(self, etree_element, parent, index, previous,
                  in_html_document, content_language=None):
@@ -110,7 +85,7 @@ class ElementWrapper:
 
     def __eq__(self, other):
         return (
-            type(self) == type(other) and
+            type(self) is type(other) and
             self.etree_element == other.etree_element)
 
     def __ne__(self, other):
@@ -132,8 +107,7 @@ class ElementWrapper:
         """
         if self._ancestors is None:
             self._ancestors = (
-                () if self.parent is None else
-                self.parent.ancestors + (self.parent,))
+                () if self.parent is None else (*self.parent.ancestors, self.parent))
         return self._ancestors
 
     @property
@@ -147,7 +121,7 @@ class ElementWrapper:
         if self._previous_siblings is None:
             self._previous_siblings = (
                 () if self.previous is None else
-                self.previous.previous_siblings + (self.previous,))
+                (*self.previous.previous_siblings, self.previous))
         return self._previous_siblings
 
     def iter_ancestors(self):
@@ -355,8 +329,7 @@ class ElementWrapper:
     def lang(self):
         """The language of this element, as a string."""
         # http://whatwg.org/C#language
-        xml_lang = self.etree_element.get(
-            '{http://www.w3.org/XML/1998/namespace}lang')
+        xml_lang = self.etree_element.get('{http://www.w3.org/XML/1998/namespace}lang')
         if xml_lang is not None:
             return ascii_lower(xml_lang)
         is_html = (
@@ -371,13 +344,11 @@ class ElementWrapper:
         # Root elememnt
         if is_html:
             content_language = None
-            iterator = self.etree_element.iter(
-                '{http://www.w3.org/1999/xhtml}meta')
+            iterator = self.etree_element.iter('{http://www.w3.org/1999/xhtml}meta')
             for meta in iterator:
                 http_equiv = meta.get('http-equiv', '')
                 if ascii_lower(http_equiv) == 'content-language':
-                    content_language = _parse_content_language(
-                        meta.get('content'))
+                    content_language = _parse_content_language(meta.get('content'))
             if content_language is not None:
                 return ascii_lower(content_language)
         # Empty string means unknown
diff -pruN 0.7.0-3/debian/changelog 0.8.0-1/debian/changelog
--- 0.7.0-3/debian/changelog	2025-03-20 12:20:17.000000000 +0000
+++ 0.8.0-1/debian/changelog	2025-08-12 21:01:18.000000000 +0000
@@ -1,3 +1,14 @@
+python-cssselect2 (0.8.0-1) unstable; urgency=low
+
+  * New upstream version 0.8.0
+  * Refresh patches.
+  * Add furo to Build-Depends, required by documentation.
+  * Bump Standards-Version to 4.7.2.
+  * Update year in d/copyright.
+  * Run wrap-and-sort -bast to reduce diff size of future changes.
+
+ -- Michael Fladischer <fladi@debian.org>  Tue, 12 Aug 2025 21:01:18 +0000
+
 python-cssselect2 (0.7.0-3) unstable; urgency=medium
 
   * Team upload.
diff -pruN 0.7.0-3/debian/control 0.8.0-1/debian/control
--- 0.7.0-3/debian/control	2025-03-20 12:17:24.000000000 +0000
+++ 0.8.0-1/debian/control	2025-08-12 21:01:18.000000000 +0000
@@ -8,6 +8,7 @@ Build-Depends:
  debhelper-compat (= 13),
  dh-python,
  flit,
+ furo,
  pybuild-plugin-pyproject,
  python-tinycss2-doc,
  python3-all,
@@ -19,7 +20,7 @@ Build-Depends:
  python3-sphinx,
  python3-sphinx-rtd-theme,
  python3-tinycss2,
-Standards-Version: 4.6.1.0
+Standards-Version: 4.7.2
 Homepage: https://github.com/Kozea/cssselect2/
 Vcs-Browser: https://salsa.debian.org/python-team/packages/python-cssselect2
 Vcs-Git: https://salsa.debian.org/python-team/packages/python-cssselect2.git
diff -pruN 0.7.0-3/debian/copyright 0.8.0-1/debian/copyright
--- 0.7.0-3/debian/copyright	2025-03-20 12:13:09.000000000 +0000
+++ 0.8.0-1/debian/copyright	2025-08-12 21:01:18.000000000 +0000
@@ -10,7 +10,7 @@ Copyright:
 License: BSD-3-clause
 
 Files: debian/*
-Copyright: 2018-2022, Michael Fladischer <fladi@debian.org>
+Copyright: 2018-2025, Michael Fladischer <fladi@debian.org>
 License: BSD-3-clause
 
 License: BSD-3-clause
diff -pruN 0.7.0-3/debian/patches/0001-Deactivate-remote-CSS-files-to-prevent-privacy-breac.patch 0.8.0-1/debian/patches/0001-Deactivate-remote-CSS-files-to-prevent-privacy-breac.patch
--- 0.7.0-3/debian/patches/0001-Deactivate-remote-CSS-files-to-prevent-privacy-breac.patch	2025-03-20 12:13:09.000000000 +0000
+++ 0.8.0-1/debian/patches/0001-Deactivate-remote-CSS-files-to-prevent-privacy-breac.patch	2025-08-12 21:01:18.000000000 +0000
@@ -3,22 +3,31 @@ Date: Tue, 21 Dec 2021 08:33:14 +0000
 Subject: Deactivate remote CSS files to prevent privacy breach.
 
 ---
- docs/conf.py | 6 +++---
- 1 file changed, 3 insertions(+), 3 deletions(-)
+ docs/conf.py | 8 ++++----
+ 1 file changed, 4 insertions(+), 4 deletions(-)
 
 diff --git a/docs/conf.py b/docs/conf.py
-index f2e7b3a..194a43d 100644
+index deb6e1c..bad6f8c 100644
 --- a/docs/conf.py
 +++ b/docs/conf.py
-@@ -51,9 +51,9 @@ html_static_path = []
+@@ -46,7 +46,7 @@ html_theme_options = {
+ }
+ 
+ # Favicon URL
+-html_favicon = 'https://www.courtbouillon.org/static/images/favicon.png'
++#html_favicon = 'https://www.courtbouillon.org/static/images/favicon.png'
+ 
+ # Add any paths that contain custom static files (such as style sheets) here,
+ # relative to this directory. They are copied after the builtin static files,
+@@ -55,9 +55,9 @@ html_static_path = []
  
  # These paths are either relative to html_static_path
  # or fully qualified paths (eg. https://...)
 -html_css_files = [
--    'https://www.courtbouillon.org/static/docs.css',
+-    'https://www.courtbouillon.org/static/docs-furo.css',
 -]
 +#html_css_files = [
-+#    'https://www.courtbouillon.org/static/docs.css',
++#    'https://www.courtbouillon.org/static/docs-furo.css',
 +#]
  
  # Output file base name for HTML help builder.
diff -pruN 0.7.0-3/debian/patches/0002-Make-intersphinx-use-local-copies-of-objects.inv-fil.patch 0.8.0-1/debian/patches/0002-Make-intersphinx-use-local-copies-of-objects.inv-fil.patch
--- 0.7.0-3/debian/patches/0002-Make-intersphinx-use-local-copies-of-objects.inv-fil.patch	2025-03-20 12:13:09.000000000 +0000
+++ 0.8.0-1/debian/patches/0002-Make-intersphinx-use-local-copies-of-objects.inv-fil.patch	2025-08-12 21:01:18.000000000 +0000
@@ -7,7 +7,7 @@ Subject: Make intersphinx use local copi
  1 file changed, 23 insertions(+), 5 deletions(-)
 
 diff --git a/docs/conf.py b/docs/conf.py
-index 194a43d..fd8fcf7 100644
+index bad6f8c..7001b96 100644
 --- a/docs/conf.py
 +++ b/docs/conf.py
 @@ -1,6 +1,9 @@
@@ -20,7 +20,7 @@ index 194a43d..fd8fcf7 100644
  
  # Add any Sphinx extension module names here, as strings. They can be
  # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-@@ -76,8 +79,23 @@ texinfo_documents = [
+@@ -80,8 +83,23 @@ texinfo_documents = [
  ]
  
  # Example configuration for intersphinx: refer to the Python standard library.
diff -pruN 0.7.0-3/debian/tests/control 0.8.0-1/debian/tests/control
--- 0.7.0-3/debian/tests/control	2025-03-20 12:13:09.000000000 +0000
+++ 0.8.0-1/debian/tests/control	2025-08-12 21:01:18.000000000 +0000
@@ -1,6 +1,8 @@
-Tests: upstream
+Tests:
+ upstream,
 Depends:
  python3-all,
  @,
  @builddeps@,
-Restrictions: allow-stderr
+Restrictions:
+ allow-stderr,
diff -pruN 0.7.0-3/docs/changelog.rst 0.8.0-1/docs/changelog.rst
--- 0.7.0-3/docs/changelog.rst	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/docs/changelog.rst	2025-03-05 14:35:04.000000000 +0000
@@ -2,6 +2,15 @@ Changelog
 ---------
 
 
+Version 0.8.0
+.............
+
+Released on 2025-03-05.
+
+* Drop support of Python 3.8 and 3.9, support 3.12 and 3.13
+* Handle case-sensitive and case-insensitive attribute selectors
+
+
 Version 0.7.0
 .............
 
diff -pruN 0.7.0-3/docs/conf.py 0.8.0-1/docs/conf.py
--- 0.7.0-3/docs/conf.py	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/docs/conf.py	2025-03-05 14:35:04.000000000 +0000
@@ -38,12 +38,16 @@ pygments_style = 'monokai'
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'sphinx_rtd_theme'
+html_theme = 'furo'
 
 html_theme_options = {
-    'collapse_navigation': False,
+    'top_of_page_buttons': ['edit'],
+    'source_edit_link': 'https://github.com/Kozea/cssselect2/edit/main/docs/{filename}',
 }
 
+# Favicon URL
+html_favicon = 'https://www.courtbouillon.org/static/images/favicon.png'
+
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
@@ -52,7 +56,7 @@ html_static_path = []
 # These paths are either relative to html_static_path
 # or fully qualified paths (eg. https://...)
 html_css_files = [
-    'https://www.courtbouillon.org/static/docs.css',
+    'https://www.courtbouillon.org/static/docs-furo.css',
 ]
 
 # Output file base name for HTML help builder.
diff -pruN 0.7.0-3/docs/contribute.rst 0.8.0-1/docs/contribute.rst
--- 0.7.0-3/docs/contribute.rst	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/docs/contribute.rst	2025-03-05 14:35:04.000000000 +0000
@@ -47,15 +47,12 @@ You can launch tests using the following
 
   venv/bin/python -m pytest
 
-cssselect2 also uses isort_ to check imports and flake8_ to check the coding
-style::
+cssselect2 also uses ruff_ to check the coding style::
 
-  venv/bin/python -m isort . --check --diff
-  venv/bin/python -m flake8 --exclude tests/css-parsing-tests
+  venv/bin/python -m ruff check
 
 .. _pytest: https://docs.pytest.org/
-.. _isort: https://pycqa.github.io/isort/
-.. _flake8: https://flake8.pycqa.org/
+.. _ruff: https://docs.astral.sh/ruff/
 
 
 Documentation
diff -pruN 0.7.0-3/docs/index.rst 0.8.0-1/docs/index.rst
--- 0.7.0-3/docs/index.rst	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/docs/index.rst	2025-03-05 14:35:04.000000000 +0000
@@ -7,14 +7,14 @@ cssselect2
 
 .. toctree::
    :caption: Documentation
-   :maxdepth: 3
+   :maxdepth: 2
 
    first_steps
    api_reference
 
 .. toctree::
    :caption: Extra Information
-   :maxdepth: 3
+   :maxdepth: 2
 
    changelog
    contribute
diff -pruN 0.7.0-3/pyproject.toml 0.8.0-1/pyproject.toml
--- 0.7.0-3/pyproject.toml	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/pyproject.toml	2025-03-05 14:35:04.000000000 +0000
@@ -8,7 +8,7 @@ description = 'CSS selectors for Python
 keywords = ['css', 'elementtree']
 authors = [{name = 'Simon Sapin', email = 'simon.sapin@exyr.org'}]
 maintainers = [{name = 'CourtBouillon', email = 'contact@courtbouillon.org'}]
-requires-python = '>=3.7'
+requires-python = '>=3.9'
 readme = {file = 'README.rst', content-type = 'text/x-rst'}
 license = {file = 'LICENSE'}
 dependencies = ['tinycss2', 'webencodings']
@@ -20,10 +20,11 @@ classifiers = [
   'Programming Language :: Python',
   'Programming Language :: Python :: 3',
   'Programming Language :: Python :: 3 :: Only',
-  'Programming Language :: Python :: 3.7',
-  'Programming Language :: Python :: 3.8',
   'Programming Language :: Python :: 3.9',
   'Programming Language :: Python :: 3.10',
+  'Programming Language :: Python :: 3.11',
+  'Programming Language :: Python :: 3.12',
+  'Programming Language :: Python :: 3.13',
   'Programming Language :: Python :: Implementation :: CPython',
   'Programming Language :: Python :: Implementation :: PyPy',
   'Topic :: Internet :: WWW/HTTP',
@@ -39,8 +40,8 @@ Changelog = 'https://github.com/Kozea/cs
 Donation = 'https://opencollective.com/courtbouillon'
 
 [project.optional-dependencies]
-doc = ['sphinx', 'sphinx_rtd_theme']
-test = ['pytest', 'isort', 'flake8']
+doc = ['sphinx', 'furo']
+test = ['pytest', 'ruff']
 
 [tool.flit.sdist]
 exclude = ['.*']
@@ -53,6 +54,9 @@ include = ['tests/*', 'cssselect2/*']
 exclude_lines = ['pragma: no cover', 'def __repr__', 'raise NotImplementedError']
 omit = ['.*']
 
-[tool.isort]
-default_section = 'FIRSTPARTY'
-multi_line_output = 4
+[tool.ruff.lint]
+select = ['E', 'W', 'F', 'I', 'N', 'RUF']
+ignore = ['RUF001', 'RUF002', 'RUF003']
+
+[tool.ruff.lint.extend-per-file-ignores]
+'docs/example.py' = ['I001']
diff -pruN 0.7.0-3/tests/test_cssselect2.py 0.8.0-1/tests/test_cssselect2.py
--- 0.7.0-3/tests/test_cssselect2.py	2022-09-19 12:50:07.000000000 +0000
+++ 0.8.0-1/tests/test_cssselect2.py	2025-03-05 14:35:04.000000000 +0000
@@ -4,10 +4,11 @@ Test suite for cssselect2.
 
 """
 
-import xml.etree.ElementTree as etree
+import xml.etree.ElementTree as etree  # noqa: N813
 from pathlib import Path
 
 import pytest
+
 from cssselect2 import ElementWrapper, SelectorError, compile_selector_list
 
 from .w3_selectors import invalid_selectors, valid_selectors
@@ -74,8 +75,7 @@ def test_valid_selectors(test):
     result = [element.id for element in root.query_all(test['selector'])]
     if result != test['expect']:  # pragma: no cover
         raise AssertionError(
-            f'{test["selector"]!r}: {result} != {test["expect"]} '
-            f'({test["name"]})')
+            f'{test["selector"]!r}: {result} != {test["expect"]} ({test["name"]})')
 
 
 def test_lang():
@@ -142,6 +142,22 @@ def test_lang():
     ('[foobar~=" \t"]', []),
     ('div[foobar~="cd"]', []),
 
+    ('a[rel="tAg"]', []),
+    ('a[rel="tAg" s]', []),
+    ('a[rel="tAg" i]', ['tag-anchor']),
+    ('a[href*="localHOST"]', []),
+    ('a[href*="localHOST" s]', []),
+    ('a[href*="localHOST" i]', ['tag-anchor']),
+    ('a[href^="hTtp"]', []),
+    ('a[href^="hTtp" s]', []),
+    ('a[href^="hTtp" i]', ['tag-anchor', 'nofollow-anchor']),
+    ('a[href$="Org"]', []),
+    ('a[href$="Org" S]', []),
+    ('a[href$="Org" I]', ['nofollow-anchor']),
+    ('div[foobar~="BC"]', []),
+    ('div[foobar~="BC" s]', []),
+    ('div[foobar~="BC" i]', ['foobar-div']),
+
     # Attribute values are case sensitive…
     ('*[lang|="En"]', ['second-li']),
     ('[lang|="En-us"]', ['second-li']),
