diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index cf40554e6f4..3666fd5a9e3 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -308,6 +308,8 @@ jobs:
         enable-cache: false
     - name: Install dependencies
       run: uv pip install . --group test
+    - name: Install Docutils' HEAD
+      run: uv pip install "docutils @ git+https://repo.or.cz/docutils.git#subdirectory=docutils"
     - name: Test with pytest
       run: python -m pytest -vv --durations 25
       env:
diff --git a/sphinx/templates/latex/latex.tex.jinja b/sphinx/templates/latex/latex.tex.jinja
index deb030504db..4ba2c46a793 100644
--- a/sphinx/templates/latex/latex.tex.jinja
+++ b/sphinx/templates/latex/latex.tex.jinja
@@ -16,6 +16,7 @@
 \ifdefined\pdfimageresolution
     \pdfimageresolution= \numexpr \dimexpr1in\relax/\sphinxpxdimen\relax
 \fi
+\newdimen\sphinxremdimen\sphinxremdimen = <%= pointsize%>
 %% let collapsible pdf bookmarks panel have high depth per default
 \PassOptionsToPackage{bookmarksdepth=5}{hyperref}
 <% if use_xindy -%>
diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty
index 7e06eff7de8..4b7b194622d 100644
--- a/sphinx/texinputs/sphinx.sty
+++ b/sphinx/texinputs/sphinx.sty
@@ -9,7 +9,7 @@
 % by the Sphinx LaTeX writer.
 
 \NeedsTeXFormat{LaTeX2e}[1995/12/01]
-\ProvidesPackage{sphinx}[2025/04/24 v8.3.0 Sphinx LaTeX package (sphinx-doc)]
+\ProvidesPackage{sphinx}[2025/06/11 v8.3.0 Sphinx LaTeX package (sphinx-doc)]
 
 % provides \ltx@ifundefined
 % (many packages load ltxcmds: graphicx does for pdftex and lualatex but
@@ -1218,5 +1218,25 @@
 % FIXME: this line should be dropped, as "9" is default anyhow.
 \ifdefined\pdfcompresslevel\pdfcompresslevel = 9 \fi
 
-
+%%% SUPPORT FOR CSS3 EXTRA LENGTH UNITS
+% cf rstdim_to_latexdim in latex.py
+%
+\def\sphinxchdimen{\dimexpr\fontcharwd\font`0\relax}
+% TODO: decide if we want rather \textwidth/\textheight.
+\newdimen\sphinxvwdimen
+  \sphinxvwdimen=\dimexpr0.01\paperwidth\relax
+\newdimen\sphinxvhdimen
+  \sphinxvhdimen=\dimexpr0.01\paperheight\relax
+\newdimen\sphinxvmindimen
+  \sphinxvmindimen=\dimexpr
+     \ifdim\paperwidth<\paperheight\sphinxvwdimen\else\sphinxvhdimen\fi
+                   \relax
+\newdimen\sphinxvmaxdimen
+  \sphinxvmaxdimen=\dimexpr
+     \ifdim\paperwidth<\paperheight\sphinxvhdimen\else\sphinxvwdimen\fi
+                   \relax
+\newdimen\sphinxQdimen
+  \sphinxQdimen=0.25mm
+% MEMO: \sphinxremdimen is defined in the template as it needs
+% the config variable pointsize.
 \endinput
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index 39aef55ddfe..0553d14c303 100644
--- a/sphinx/writers/latex.py
+++ b/sphinx/writers/latex.py
@@ -305,6 +305,11 @@ def escape_abbr(text: str) -> str:
 
 def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
     """Convert `width_str` with rst length to LaTeX length."""
+    # MEMO: the percent unit is interpreted here as a percentage
+    # of \linewidth.  Let's keep in mind though that \linewidth
+    # is dynamic in LaTeX (e.g. smaller in lists), and may even
+    # be zero in some contexts (some table cells).  This is a legacy
+    # situation perhaps best not changed (2025/06/11).
     match = re.match(r'^(\d*\.?\d*)\s*(\S*)$', width_str)
     if not match:
         raise ValueError
@@ -318,6 +323,8 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
             res = '%sbp' % amount  # convert to 'bp'
         elif unit == '%':
             res = r'%.3f\linewidth' % (float(amount) / 100.0)
+        elif unit in {'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'Q'}:
+            res = rf'{amount}\sphinx{unit}dimen'
     else:
         amount_float = float(amount) * scale / 100.0
         if unit in {'', 'px'}:
@@ -326,8 +333,16 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
             res = '%.5fbp' % amount_float
         elif unit == '%':
             res = r'%.5f\linewidth' % (amount_float / 100.0)
+        elif unit in {'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'Q'}:
+            res = rf'{amount_float:.5f}\sphinx{unit}dimen'
         else:
             res = f'{amount_float:.5f}{unit}'
+    # MEMO: non-recognized units will in all probability end up causing
+    # a low-level TeX error.  The units not among those above which will
+    # be accepted by TeX are sp (all TeX dimensions are integer multiple
+    # of 1sp), em and ex (font dependent), bp, cm, mm, in, and pc.
+    # Non-CSS units are cc, nc, dd, and nd. Also the math only mu, which
+    # is not usable for example for LaTeX length assignments.
     return res
 
 
diff --git a/tests/roots/test-latex-images-css3-lengths/conf.py b/tests/roots/test-latex-images-css3-lengths/conf.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/roots/test-latex-images-css3-lengths/img.png b/tests/roots/test-latex-images-css3-lengths/img.png
new file mode 100644
index 00000000000..a97e86d66af
Binary files /dev/null and b/tests/roots/test-latex-images-css3-lengths/img.png differ
diff --git a/tests/roots/test-latex-images-css3-lengths/index.rst b/tests/roots/test-latex-images-css3-lengths/index.rst
new file mode 100644
index 00000000000..52255262b1c
--- /dev/null
+++ b/tests/roots/test-latex-images-css3-lengths/index.rst
@@ -0,0 +1,25 @@
+=============
+ TEST IMAGES
+=============
+
+test-latex-images-css3-lengths
+==============================
+
+.. image:: img.png
+   :width: 10.03ch
+   :height: 9.97rem
+
+.. image:: img.png
+   :width: 60vw
+   :height: 10vh
+
+.. image:: img.png
+   :width: 10.5vmin
+   :height: 10.5vmax
+
+.. image:: img.png
+   :width: 195.345Q
+
+.. image:: img.png
+   :width: 195.345Q
+   :scale: 50%
diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py
index 16f3437c154..0e8dd4e004b 100644
--- a/tests/test_builders/test_build_latex.py
+++ b/tests/test_builders/test_build_latex.py
@@ -55,7 +55,9 @@ def kpsetest(*filenames):
 
 
 # compile latex document with app.config.latex_engine
-def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manual'):
+def compile_latex_document(
+    app, filename='projectnamenotset.tex', docclass='manual', runtwice=False
+):
     # now, try to run latex over it
     try:
         with chdir(app.outdir):
@@ -82,7 +84,7 @@ def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manu
             # as configured in the Makefile and in presence of latexmkrc
             # or latexmkjarc and also sphinx.xdy and other xindy support.
             # And two passes are not enough except for simplest documents.
-            if app.config.latex_engine == 'pdflatex':
+            if runtwice:
                 subprocess.run(args, capture_output=True, check=True)
     except OSError as exc:  # most likely the latex executable was not found
         raise pytest.skip.Exception from exc
@@ -101,6 +103,10 @@ def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manu
     not kpsetest(*STYLEFILES),
     reason='not running latex, the required styles do not seem to be installed',
 )
+skip_if_docutils_not_at_least_at_0_22 = pytest.mark.skipif(
+    docutils.__version_info__[:2] < (0, 22),
+    reason='this test requires Docutils at least at 0.22',
+)
 
 
 class RemoteImageHandler(http.server.BaseHTTPRequestHandler):
@@ -128,17 +134,17 @@ def do_GET(self):
 @skip_if_requested
 @skip_if_stylefiles_notfound
 @pytest.mark.parametrize(
-    ('engine', 'docclass', 'python_maximum_signature_line_length'),
+    ('engine', 'docclass', 'python_maximum_signature_line_length', 'runtwice'),
     # Only running test with `python_maximum_signature_line_length` not None with last
     # LaTeX engine to reduce testing time, as if this configuration does not fail with
     # one engine, it's almost impossible it would fail with another.
     [
-        ('pdflatex', 'manual', None),
-        ('pdflatex', 'howto', None),
-        ('lualatex', 'manual', None),
-        ('lualatex', 'howto', None),
-        ('xelatex', 'manual', 1),
-        ('xelatex', 'howto', 1),
+        ('pdflatex', 'manual', None, True),
+        ('pdflatex', 'howto', None, True),
+        ('lualatex', 'manual', None, False),
+        ('lualatex', 'howto', None, False),
+        ('xelatex', 'manual', 1, False),
+        ('xelatex', 'howto', 1, False),
     ],
 )
 @pytest.mark.sphinx(
@@ -146,7 +152,9 @@ def do_GET(self):
     testroot='root',
     freshenv=True,
 )
-def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_length):
+def test_build_latex_doc(
+    app, engine, docclass, python_maximum_signature_line_length, runtwice
+):
     app.config.python_maximum_signature_line_length = (
         python_maximum_signature_line_length
     )
@@ -170,7 +178,23 @@ def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_le
     # file from latex_additional_files
     assert (app.outdir / 'svgimg.svg').is_file()
 
-    compile_latex_document(app, 'sphinxtests.tex', docclass)
+    compile_latex_document(app, 'sphinxtests.tex', docclass, runtwice)
+
+
+@skip_if_requested
+@skip_if_stylefiles_notfound
+@skip_if_docutils_not_at_least_at_0_22
+@pytest.mark.parametrize('engine', ['pdflatex', 'lualatex', 'xelatex'])
+@pytest.mark.sphinx(
+    'latex',
+    testroot='latex-images-css3-lengths',
+)
+def test_build_latex_with_css3_lengths(app, engine):
+    app.config.latex_engine = engine
+    app.config.latex_documents = [(*app.config.latex_documents[0][:4], 'howto')]
+    app.builder.init()
+    app.build(force_all=True)
+    compile_latex_document(app, docclass='howto')
 
 
 @pytest.mark.sphinx('latex', testroot='root')