diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index 8af9df276194169257f26c7251cc86ff049d0ad5..0000000000000000000000000000000000000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-SPHINXPROJ    = GWDataFind
-SOURCEDIR     = .
-BUILDDIR      = _build
-
-.PHONY: help html
-
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-html:
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/conf.py b/docs/conf.py
index 7ab859922654b4d8f37acdbdfd69cf860fd47cf5..9e29178e147e1f7b3bbb41202816d755d44bca60 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,44 +1,66 @@
-#
 # gwdatafind documentation build configuration file
 
-import glob
-import os.path
+import inspect
 import re
+import sys
+from os import getenv
+from pathlib import Path
 
-from gwdatafind import __version__ as gwdatafind_version
+import gwdatafind
 
-extensions = [
-    'sphinx.ext.intersphinx',
-    'sphinx.ext.napoleon',
-    'sphinx_automodapi.automodapi',
-    'sphinxarg.ext',
-]
+# -- metadata
+
+project = "gwdatafind"
+copyright = "2018-2025, Cardiff University"
+author = "Duncan Macleod"
+release = gwdatafind.__version__
+version = re.split(r'[\w-]', gwdatafind.__version__)[0]
 
-#templates_path = ['_templates']
+# -- config
 
 source_suffix = '.rst'
-
 master_doc = 'index'
 
-# General information about the project.
-project = "gwdatafind"
-copyright = "2018-2021, Cardiff University"
-author = "Duncan Macleod"
+default_role = 'obj'
+
+# -- theme
+
+html_theme = "furo"
+
+html_theme_options = {
+    "footer_icons": [
+        {
+            "name": "GitLab",
+            "url": "https://git.ligo.org/computing/gwdatafind/client",
+            "class": "fa-brands fa-gitlab",
+        },
+    ],
+}
 
-# The short X.Y version.
-version = re.split(r'[\w-]', gwdatafind_version)[0]
-# The full version, including alpha/beta/rc tags.
-release = gwdatafind_version
+# need fontawesome for the gitlab icon in the footer
+html_css_files = [
+    "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/fontawesome.min.css",
+    "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/solid.min.css",
+    "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/brands.min.css",
+]
 
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+pygments_dark_style = "monokai"
 
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'monokai'
+# -- extensions
 
-# Intersphinx directory
+extensions = [
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.napoleon",
+    "sphinx.ext.linkcode",
+    "sphinx_automodapi.automodapi",
+    "sphinx_copybutton",
+    "sphinxarg.ext",
+]
+
+# automodapi
+automodapi_inherited_members = False
+
+# intersphinx
 intersphinx_mapping = {
     "igwn-auth-utils": (
         "https://igwn-auth-utils.readthedocs.io/en/stable/",
@@ -62,49 +84,75 @@ intersphinx_mapping = {
     ),
 }
 
-# The reST default role (used for this markup: `text`) to use for all
-# documents.
-default_role = 'obj'
-
-# napoleon configuration
+# napoleon
 napoleon_use_rtype = False
 
-# Don't inherit in automodapi
-numpydoc_show_class_members = False
-automodapi_inherited_members = False
-
-# -- Options for HTML output ----------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-#
-html_theme = 'sphinx_rtd_theme'
-
-# 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".
-html_static_path = ['_static']
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'gwdatafinddoc'
-
-# -- add static files----------------------------------------------------------
-
-def setup_static_content(app):
-    curdir = os.path.abspath(os.path.dirname(__file__))
-    # configure stylesheets
-    for sdir in html_static_path:
-        staticdir = os.path.join(curdir, sdir)
-
-        # add stylesheets
-        for cssf in glob.glob(os.path.join(staticdir, 'css', '*.css')):
-            app.add_css_file(cssf[len(staticdir)+1:])
-
-        # add custom javascript
-        for jsf in glob.glob(os.path.join(staticdir, 'js', '*.js')):
-            app.add_js_file(jsf[len(staticdir)+1:])
-
-# -- setup --------------------------------------------------------------------
 
-def setup(app):
-    setup_static_content(app)
+# -- linkcode
+
+def _project_git_ref(version, prefix="v"):
+    """Returns the git reference for the given full release version.
+    """
+    # handle builds in CI
+    if getenv("GITLAB_CI"):
+        return getenv("CI_COMMIT_REF")
+    if getenv("GITHUB_ACTIONS"):
+        return getenv("GITHUB_SHA")
+    # otherwise use the project metadata
+    _setuptools_scm_version_regex = re.compile(
+        r"\+g(\w+)(?:\Z|\.)",
+    )
+    if match := _setuptools_scm_version_regex.search(version):
+        return match.groups()[0]
+    return f"{prefix}{version}"
+
+
+PROJECT_GIT_REF = _project_git_ref(release, prefix="")
+PROJECT_PATH = Path(gwdatafind.__file__).parent
+PROJECT_URL = getenv(
+    "CI_PROJECT_URL",
+    "https://git.ligo.org/computing/gwdatafind/client",
+)
+PROJECT_BLOB_URL = f"{PROJECT_URL}/blob/{PROJECT_GIT_REF}/{PROJECT_PATH.name}"
+
+
+def linkcode_resolve(domain, info):
+    """Determine the URL corresponding to Python object.
+    """
+    if domain != "py" or not info["module"]:
+        return None
+
+    def find_source(module, fullname):
+        """Construct a source file reference for an object reference.
+        """
+        # resolve object
+        obj = sys.modules[module]
+        for part in fullname.split("."):
+            obj = getattr(obj, part)
+        # get filename relative to project
+        filename = Path(
+            inspect.getsourcefile(obj),  # type: ignore [arg-type]
+        ).relative_to(PROJECT_PATH).as_posix()
+        # get line numbers of this object
+        lines, lineno = inspect.findsource(obj)
+        if lineno:
+            start = lineno + 1  # 0-index
+            end = lineno + len(inspect.getblock(lines[lineno:]))
+        else:
+            start = end = 0
+        return filename, start, end
+
+    try:
+        path, start, end = find_source(info["module"], info["fullname"])
+    except (
+        AttributeError,  # object not found
+        OSError,  # file not found
+        TypeError,  # source for object not found
+        ValueError,  # file not from this project
+    ):
+        return None
+
+    url = f"{PROJECT_BLOB_URL}/{path}"
+    if start:
+        url += f"#L{start}-L{end}"
+    return url
diff --git a/pyproject.toml b/pyproject.toml
index 1801bd37cc7dfbf705d07d05d4dcf1e6ec285023..9db2a4fd29c248adfd1e81002f10d6ccb036685d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,11 +44,12 @@ dynamic = [
 
 [project.optional-dependencies]
 docs = [
+  "furo",
   "numpydoc",
-  "sphinx >= 4.4.0",
+  "Sphinx >= 4.4.0",
   "sphinx-argparse",
   "sphinx_automodapi",
-  "sphinx_rtd_theme",
+  "sphinx-copybutton",
 ]
 test = [
   "pytest >= 2.8.0",