diff --git a/pygitweb/main.py b/pygitweb/main.py
index 59c06c6..ba4b5de 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -683,20 +683,7 @@ def git_blob(project: str, h: str | None, f: str | None, *, raw: bool = False) -
         f"<pre class=\"blob-content\"><code class=\"hljs{lang_attr}\">{body}</code></pre>"
         f"</div>"
     )
-    blob_script = """
-<script>
-(function(){
-  var el = document.getElementById('Highlight-script');
-  function run() {
-    var code = document.querySelector('.blob-content code');
-    if (code && typeof hljs !== 'undefined') hljs.highlightElement(code);
-  }
-  if (el) {
-    if (el.getAttribute('data-loaded')) run();
-    else el.addEventListener('load', function(){ el.setAttribute('data-loaded','1'); run(); });
-  }
-})();
-</script>"""
+    blob_script = '<script src="static/blob-view.js"></script>'
     pre = PREAMBLE.render(title=f"{esc_html(f)} - {esc_html(project)}", theme="dark", site_name=config.SITE_NAME)
     return HTMLResponse(f"{pre}{blob_html}{blob_script}{POSTAMBLE}")
 
diff --git a/pygitweb/static/blob-view.js b/pygitweb/static/blob-view.js
new file mode 100644
index 0000000..9b6d96f
--- /dev/null
+++ b/pygitweb/static/blob-view.js
@@ -0,0 +1,133 @@
+(function(){
+  var el = document.getElementById('Highlight-script');
+  function run() {
+    var code = document.querySelector('.blob-content code');
+    if (code && typeof hljs !== 'undefined') hljs.highlightElement(code);
+    applyBlobLineSelection();
+  }
+  function parseLinesParam() {
+    var params = new URLSearchParams(window.location.search);
+    var s = params.get('lines');
+    if (!s) return [];
+    var re = /L(\d+)(?:C(\d+))?(?:-L(\d+)(?:C(\d+))?)?/g;
+    var ranges = [];
+    var m;
+    while ((m = re.exec(s)) !== null) {
+      var startLine = parseInt(m[1], 10);
+      var startCol = m[2] ? parseInt(m[2], 10) : null;
+      var endLine = m[3] !== undefined ? parseInt(m[3], 10) : null;
+      var endCol = m[4] ? parseInt(m[4], 10) : null;
+      ranges.push({ startLine: startLine, startCol: startCol, endLine: endLine, endCol: endCol });
+    }
+    return ranges;
+  }
+  function wrapCodeInLineSpans(code) {
+    var lines = [];
+    var current = [];
+    function flushLine() {
+      if (current.length) {
+        lines.push(current);
+        current = [];
+      }
+    }
+    var nodes = Array.from(code.childNodes);
+    for (var i = 0; i < nodes.length; i++) {
+      var node = nodes[i];
+      if (node.nodeType === Node.TEXT_NODE) {
+        var parts = node.textContent.split('\n');
+        for (var j = 0; j < parts.length; j++) {
+          if (j > 0) flushLine();
+          current.push(parts[j]);
+        }
+      } else {
+        current.push(node);
+      }
+    }
+    flushLine();
+    code.replaceChildren();
+    for (var i = 0; i < lines.length; i++) {
+      var span = document.createElement('span');
+      span.setAttribute('data-line', String(i + 1));
+      for (var k = 0; k < lines[i].length; k++) {
+        var item = lines[i][k];
+        if (typeof item === 'string') {
+          span.appendChild(document.createTextNode(item));
+        } else {
+          span.appendChild(item);
+        }
+      }
+      code.appendChild(span);
+      if (i < lines.length - 1) code.appendChild(document.createTextNode('\n'));
+    }
+    return code.querySelectorAll('[data-line]');
+  }
+  function wrapCharacterRangeInSpan(lineSpan, startChar, endChar) {
+    startChar = Math.max(0, startChar);
+    var len = lineSpan.textContent.length;
+    endChar = endChar == null ? len : Math.min(len, endChar);
+    if (startChar >= endChar) return;
+    var walker = document.createTreeWalker(lineSpan, NodeFilter.SHOW_TEXT);
+    var charIdx = 0;
+    var startNode = null, startOff = 0, endNode = null, endOff = 0;
+    var n;
+    while ((n = walker.nextNode())) {
+      var nLen = n.length;
+      if (startNode == null && charIdx + nLen > startChar) {
+        startNode = n;
+        startOff = startChar - charIdx;
+      }
+      if (endNode == null && charIdx + nLen >= endChar) {
+        endNode = n;
+        endOff = endChar - charIdx;
+        break;
+      }
+      charIdx += nLen;
+    }
+    if (!startNode || !endNode) return;
+    try {
+      var range = document.createRange();
+      range.setStart(startNode, startOff);
+      range.setEnd(endNode, endOff);
+      var span = document.createElement('span');
+      span.className = 'blob-selection';
+      range.surroundContents(span);
+    } catch (e) {}
+  }
+  function applyBlobLineSelection() {
+    var code = document.querySelector('.blob-content code');
+    if (!code) return;
+    var ranges = parseLinesParam();
+    if (!ranges.length) return;
+    var lineSpans = wrapCodeInLineSpans(code);
+    var firstTarget = null;
+    for (var r = 0; r < ranges.length; r++) {
+      var range = ranges[r];
+      var startLine = range.startLine;
+      var startCol = range.startCol;
+      var endLine = range.endLine != null ? range.endLine : startLine;
+      var endCol = range.endCol;
+      for (var i = startLine; i <= endLine; i++) {
+        var span = code.querySelector('[data-line="' + i + '"]');
+        if (!span) continue;
+        if (firstTarget === null) firstTarget = span;
+        var lineLen = span.textContent.length;
+        var startCh = (i === startLine && startCol != null) ? startCol - 1 : 0;
+        var endCh = (i === endLine && endCol != null) ? endCol : lineLen;
+        if (startCh === 0 && endCh >= lineLen) {
+          span.classList.add('blob-line-selected');
+        } else {
+          wrapCharacterRangeInSpan(span, startCh, endCh);
+        }
+      }
+    }
+    if (firstTarget) {
+      firstTarget.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+    }
+  }
+  if (el) {
+    if (el.getAttribute('data-loaded')) run();
+    else el.addEventListener('load', function(){ el.setAttribute('data-loaded','1'); run(); });
+  } else {
+    run();
+  }
+})();
diff --git a/pygitweb/static/main.css b/pygitweb/static/main.css
index eee173d..2df553f 100644
--- a/pygitweb/static/main.css
+++ b/pygitweb/static/main.css
@@ -164,3 +164,9 @@ tbody tr:hover {
 	font-size: inherit;
 	line-height: inherit;
 }
+
+/* Line selection from URL param lines= (client-side) */
+.blob-view .blob-line-selected,
+.blob-view .blob-selection {
+	background-color: var(--pgw-blob-selection-bg, rgba(193, 173, 0, 0.35));
+}
