diff --git a/metadata.json b/metadata.json
new file mode 100644
index 0000000..18aee20
--- /dev/null
+++ b/metadata.json
@@ -0,0 +1,10 @@
+{
+ "author": "spleenftw",
+ "email": "",
+ "website": "https://github.com/spleenftw/bludit-toc",
+ "version": "1.0.0",
+ "releaseDate": "2026-06-04",
+ "license": "MIT",
+ "compatible": "3.22.0",
+ "notes": "Sticky Table of Contents sidebar (desktop) and floating drawer (mobile) built from the article's h2/h3/h4 headings."
+}
diff --git a/plugin.php b/plugin.php
new file mode 100644
index 0000000..4f9c794
--- /dev/null
+++ b/plugin.php
@@ -0,0 +1,93 @@
+dbFields = array(
+ // Label shown at the top of the sidebar and in the mobile drawer.
+ 'title' => 'On this page',
+ // Pixel offset to clear the fixed navbar when jumping to a heading.
+ 'navbarHeight' => 80,
+ // Viewport width (px) at which the fixed sidebar appears.
+ // Below this width the mobile floating-button drawer is used instead.
+ 'minWidth' => 1280,
+ );
+ }
+
+ // ---- Admin settings form ----
+
+ public function form() {
+ global $L;
+
+ $title = htmlspecialchars($this->getDbField('title'), ENT_QUOTES, 'UTF-8');
+ $navbarHeight = (int) $this->getDbField('navbarHeight');
+ $minWidth = (int) $this->getDbField('minWidth');
+
+ $h = '';
+
+ $h .= '
';
+ $h .= '';
+ $h .= '';
+ $h .= '
';
+
+ $h .= '';
+ $h .= '';
+ $h .= '';
+ $h .= '' . $L->get('Used to offset hash-jump anchors so they clear the fixed navbar.') . '';
+ $h .= '
';
+
+ $h .= '';
+ $h .= '';
+ $h .= '';
+ $h .= '' . $L->get('Viewport widths below this value use the mobile floating button instead.') . '';
+ $h .= '
';
+
+ return $h;
+ }
+
+ // ---- Front-end hooks ----
+
+ /**
+ * Inject the plugin CSS in .
+ */
+ public function siteHead() {
+ $url = DOMAIN_PLUGINS . $this->directoryName . '/toc.css';
+ return HTML::style($url) . PHP_EOL;
+ }
+
+ /**
+ * Inject the ToC sidebar container right after .
+ * It is position:fixed, so placement in the DOM doesn't affect layout.
+ * JS will populate