diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2589f911..496739944 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 - Fixed outline not rendering correctly in some scenarios (e.g. on Button widgets) https://github.com/Textualize/textual/issues/3628
 - Fixed live-reloading of screen CSS https://github.com/Textualize/textual/issues/3454
 - `Select.value` could be in an invalid state https://github.com/Textualize/textual/issues/3612
+- Off-by-one in CSS error reporting https://github.com/Textualize/textual/issues/3625
 
 ### Added
 
diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py
index d7cfc449d..875537eb7 100644
--- a/src/textual/css/tokenizer.py
+++ b/src/textual/css/tokenizer.py
@@ -34,9 +34,9 @@ class TokenError(Exception):
         Args:
             read_from: The location where the CSS was read from.
             code: The code being parsed.
-            start: Line number of the error.
+            start: Line and column number of the error (1-indexed).
             message: A message associated with the error.
-            end: End location of token, or None if not known.
+            end: End location of token (1-indexed), or None if not known.
         """
 
         self.read_from = read_from
@@ -60,9 +60,13 @@ class TokenError(Exception):
             line_numbers=True,
             indent_guides=True,
             line_range=(max(0, line_no - 2), line_no + 2),
-            highlight_lines={line_no + 1},
+            highlight_lines={line_no},
+        )
+        syntax.stylize_range(
+            "reverse bold",
+            (self.start[0], self.start[1] - 1),
+            (self.end[0], self.end[1] - 1),
         )
-        syntax.stylize_range("reverse bold", self.start, self.end)
         return Panel(syntax, border_style="red")
 
     def __rich__(self) -> RenderableType:
@@ -136,19 +140,20 @@ class Token(NamedTuple):
     read_from: CSSLocation
     code: str
     location: tuple[int, int]
+    """Token starting location, 0-indexed."""
     referenced_by: ReferencedBy | None = None
 
     @property
     def start(self) -> tuple[int, int]:
-        """Start line and column (1 indexed)."""
+        """Start line and column (1-indexed)."""
         line, offset = self.location
-        return (line + 1, offset)
+        return (line + 1, offset + 1)
 
     @property
     def end(self) -> tuple[int, int]:
-        """End line and column (1 indexed)."""
+        """End line and column (1-indexed)."""
         line, offset = self.location
-        return (line + 1, offset + len(self.value))
+        return (line + 1, offset + len(self.value) + 1)
 
     def with_reference(self, by: ReferencedBy | None) -> "Token":
         """Return a copy of the Token, with reference information attached.
@@ -199,7 +204,7 @@ class Tokenizer:
                     "",
                     self.read_from,
                     self.code,
-                    (line_no + 1, col_no + 1),
+                    (line_no, col_no),
                     None,
                 )
             else:
@@ -217,7 +222,7 @@ class Tokenizer:
             raise TokenError(
                 self.read_from,
                 self.code,
-                (line_no, col_no),
+                (line_no + 1, col_no + 1),
                 message,
             )
         iter_groups = iter(match.groups())
@@ -251,14 +256,14 @@ class Tokenizer:
                 raise TokenError(
                     self.read_from,
                     self.code,
-                    (line_no, col_no),
+                    (line_no + 1, col_no + 1),
                     f"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}",
                 )
             else:
                 raise TokenError(
                     self.read_from,
                     self.code,
-                    (line_no, col_no),
+                    (line_no + 1, col_no + 1),
                     f"unknown pseudo-class {pseudo_class!r}; {all_valid}",
                 )
 

diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py
index 124f820d5..febe4f06b 100644
--- a/tests/css/test_parse.py
+++ b/tests/css/test_parse.py
@@ -1238,7 +1238,7 @@ class TestTypeNames:
                 stylesheet.parse()
 
 
-def test_parse_bad_psuedo_selector():
+def test_parse_bad_pseudo_selector():
     """Check unknown selector raises a token error."""
 
     bad_selector = """\
@@ -1248,9 +1248,27 @@ Widget:foo{
     """
 
     stylesheet = Stylesheet()
-    stylesheet.add_source(bad_selector, "foo")
+    stylesheet.add_source(bad_selector, None)
 
     with pytest.raises(TokenError) as error:
         stylesheet.parse()
 
-    assert error.value.start == (0, 6)
+    assert error.value.start == (1, 7)
+
+
+def test_parse_bad_pseudo_selector_with_suggestion():
+    """Check unknown pseudo selector raises token error with correct position."""
+
+    bad_selector = """
+Widget:blu {
+    border: red;
+}
+"""
+
+    stylesheet = Stylesheet()
+    stylesheet.add_source(bad_selector, None)
+
+    with pytest.raises(TokenError) as error:
+        stylesheet.parse()
+
+    assert error.value.start == (2, 7)

CSS error reporting sometimes off by one
See https://github.com/Textualize/textual/pull/3582#issuecomment-1787507687.

Running the app at the bottom produces the error below, where the error reporting is off by one.
See the line above the panel and the code lines in the code snippet printed.

```
 Error in stylesheet:
 /Users/davep/develop/python/textual-upstream/sandbox/foo.py, CSSErrorApp.CSS:1:4
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│   1 │                                                                                                                                                                                                                      │
│ ❱ 2 │   :                                                                                                                                                                                                                  │
│   3                                                                                                                                                                                                                        │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
 • Expected one of 'comment line', 'comment start', 'selector start', 'selector start class', 'selector start id', 'selector start universal', 'variable name', or 'whitespace'.                                              
 • Did you forget a semicolon at the end of a line?      
```

```py
from textual.app import App, ComposeResult
from textual.widgets import Label

class CSSErrorApp(App[None]):

    CSS = """
    :
    """

    def compose(self) -> ComposeResult:
        yield Label()

if __name__ == "__main__":
    CSSErrorApp().run()
```
tests/css/test_parse.py::test_parse_bad_pseudo_selector tests/css/test_parse.py::test_parse_bad_pseudo_selector_with_suggestion
tests/css/test_parse.py::TestVariableReferenceSubstitution::test_simple_reference tests/css/test_parse.py::TestVariableReferenceSubstitution::test_simple_reference_no_whitespace tests/css/test_parse.py::TestVariableReferenceSubstitution::test_undefined_variable tests/css/test_parse.py::TestVariableReferenceSubstitution::test_empty_variable tests/css/test_parse.py::TestVariableReferenceSubstitution::test_transitive_reference tests/css/test_parse.py::TestVariableReferenceSubstitution::test_multi_value_variable tests/css/test_parse.py::TestVariableReferenceSubstitution::test_variable_used_inside_property_value tests/css/test_parse.py::TestVariableReferenceSubstitution::test_variable_definition_eof tests/css/test_parse.py::TestVariableReferenceSubstitution::test_variable_reference_whitespace_trimming tests/css/test_parse.py::TestParseLayout::test_valid_layout_name tests/css/test_parse.py::TestParseLayout::test_invalid_layout_name tests/css/test_parse.py::TestParseText::test_foreground tests/css/test_parse.py::TestParseText::test_background tests/css/test_parse.py::TestParseColor::test_rgb_and_hsl[rgb(1,255,50)-result0] tests/css/test_parse.py::TestParseColor::test_rgb_and_hsl[rgb( tests/css/test_parse.py::TestParseColor::test_rgb_and_hsl[rgba( tests/css/test_parse.py::TestParseColor::test_rgb_and_hsl[hsl( tests/css/test_parse.py::TestParseColor::test_rgb_and_hsl[hsl(180,50%,50%)-result5] tests/css/test_parse.py::TestParseColor::test_rgb_and_hsl[hsla(180,50%,50%,0.25)-result6] tests/css/test_parse.py::TestParseColor::test_rgb_and_hsl[hsla( tests/css/test_parse.py::TestParseOffset::test_composite_rule[-5.5%-parsed_x0--30%-parsed_y0] tests/css/test_parse.py::TestParseOffset::test_composite_rule[5%-parsed_x1-40%-parsed_y1] tests/css/test_parse.py::TestParseOffset::test_composite_rule[10-parsed_x2-40-parsed_y2] tests/css/test_parse.py::TestParseOffset::test_separate_rules[-5.5%-parsed_x0--30%-parsed_y0] tests/css/test_parse.py::TestParseOffset::test_separate_rules[5%-parsed_x1-40%-parsed_y1] tests/css/test_parse.py::TestParseOffset::test_separate_rules[-10-parsed_x2-40-parsed_y2] tests/css/test_parse.py::TestParseOverflow::test_multiple_enum tests/css/test_parse.py::TestParseTransition::test_various_duration_formats[5.57s-5.57] tests/css/test_parse.py::TestParseTransition::test_various_duration_formats[0.5s-0.5] tests/css/test_parse.py::TestParseTransition::test_various_duration_formats[1200ms-1.2] tests/css/test_parse.py::TestParseTransition::test_various_duration_formats[0.5ms-0.0005] tests/css/test_parse.py::TestParseTransition::test_various_duration_formats[20-20.0] tests/css/test_parse.py::TestParseTransition::test_various_duration_formats[0.1-0.1] tests/css/test_parse.py::TestParseTransition::test_no_delay_specified tests/css/test_parse.py::TestParseTransition::test_unknown_easing_function tests/css/test_parse.py::TestParseOpacity::test_opacity_to_styles[-0.2-0.0] tests/css/test_parse.py::TestParseOpacity::test_opacity_to_styles[0.4-0.4] tests/css/test_parse.py::TestParseOpacity::test_opacity_to_styles[1.3-1.0] tests/css/test_parse.py::TestParseOpacity::test_opacity_to_styles[-20%-0.0] tests/css/test_parse.py::TestParseOpacity::test_opacity_to_styles[25%-0.25] tests/css/test_parse.py::TestParseOpacity::test_opacity_to_styles[128%-1.0] tests/css/test_parse.py::TestParseOpacity::test_opacity_invalid_value tests/css/test_parse.py::TestParseMargin::test_margin_partial tests/css/test_parse.py::TestParsePadding::test_padding_partial tests/css/test_parse.py::TestParseTextAlign::test_text_align[left] tests/css/test_parse.py::TestParseTextAlign::test_text_align[start] tests/css/test_parse.py::TestParseTextAlign::test_text_align[center] tests/css/test_parse.py::TestParseTextAlign::test_text_align[right] tests/css/test_parse.py::TestParseTextAlign::test_text_align[end] tests/css/test_parse.py::TestParseTextAlign::test_text_align[justify] tests/css/test_parse.py::TestParseTextAlign::test_text_align_invalid tests/css/test_parse.py::TestTypeNames::test_type_no_number tests/css/test_parse.py::TestTypeNames::test_type_with_number tests/css/test_parse.py::TestTypeNames::test_type_starts_with_number tests/css/test_parse.py::TestTypeNames::test_combined_type_no_number tests/css/test_parse.py::TestTypeNames::test_combined_type_with_number tests/css/test_parse.py::TestTypeNames::test_combined_type_starts_with_number