Repository: Textualize/textual

Issue:
Nesting selectors in a selector list causes a stylesheet error
If you have a stylesheet like this:

```python
from textual.app import App, ComposeResult
from textual.widgets import Label

class NestedCSSTokenErrorApp(App[None]):

    CSS = """
    Label {
        &.foo, &.bar {
            border: solid red;
        }
    }
    """

    def compose(self) -> ComposeResult:
        yield Label("This is class foo", classes="foo")
        yield Label("This is class bar", classes="bar")

if __name__ == "__main__":
    NestedCSSTokenErrorApp().run()
```

the result is:

```
Error in stylesheet:
 /Users/davep/develop/python/textual-sandbox/nested_css_token_error.py, NestedCSSTokenErrorApp.CSS:3:16
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│   1 │                                                                                                                                               │
│   2 │   Label {                                                                                                                                     │
│ ❱ 3 │   │   &.foo, &.bar {                                                                                                                          │
│   4 │   │   │   border: solid red;                                                                                                                  │
│   5 │   │   }                                                                                                                                       │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
 • Expected selector or { (found '&.bar {').                                                                                                           
 • Did you forget a semicolon at the end of a line? 
```

This feels like a bug given that it's a slightly different way of writing this:

```python
from textual.app import App, ComposeResult
from textual.widgets import Label

class NestedCSSTokenErrorApp(App[None]):

    CSS = """
    Label {
        &.foo {
            border: solid red;
        }
        &.bar {
            border: solid red;
        }
    }
    """

    def compose(self) -> ComposeResult:
        yield Label("This is class foo", classes="foo")
        yield Label("This is class bar", classes="bar")

if __name__ == "__main__":
    NestedCSSTokenErrorApp().run()
```

Tagging @willmcgugan for triage.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bb816e2a..c975d2f84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 - Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124
 
+### Fixed
+
+- Improve support for selector lists in nested TCSS https://github.com/Textualize/textual/issues/3969
+- Improve support for rule declarations after nested TCSS rule sets https://github.com/Textualize/textual/issues/3999
+
 ## [0.50.1] - 2024-02-09
 
 ### Fixed
diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py
index 0d0df4000..b029fad55 100644
--- a/src/textual/css/tokenize.py
+++ b/src/textual/css/tokenize.py
@@ -64,6 +64,7 @@ expect_root_nested = Expect(
     whitespace=r"\s+",
     comment_start=COMMENT_START,
     comment_line=COMMENT_LINE,
+    declaration_name=r"[a-zA-Z_\-]+\:",
     selector_start_id=r"\#" + IDENTIFIER,
     selector_start_class=r"\." + IDENTIFIER,
     selector_start_universal=r"\*",
@@ -105,6 +106,7 @@ expect_selector_continue = Expect(
     new_selector=r",",
     declaration_set_start=r"\{",
     declaration_set_end=r"\}",
+    nested=r"\&",
 ).expect_eof(True)
 
 # A rule declaration e.g. "text: red;"


Test patch:
diff --git a/tests/css/test_nested_css.py b/tests/css/test_nested_css.py
index 821c655ff..32086f2e4 100644
--- a/tests/css/test_nested_css.py
+++ b/tests/css/test_nested_css.py
@@ -44,6 +44,53 @@ async def test_nest_app():
         assert app.query_one("#foo .paul").styles.background == Color.parse("blue")
 
 
+class ListOfNestedSelectorsApp(App[None]):
+    CSS = """
+    Label {
+        &.foo, &.bar {
+            background: red;
+        }
+    }
+    """
+
+    def compose(self) -> ComposeResult:
+        yield Label("one", classes="foo")
+        yield Label("two", classes="bar")
+        yield Label("three", classes="heh")
+
+
+async def test_lists_of_selectors_in_nested_css() -> None:
+    """Regression test for https://github.com/Textualize/textual/issues/3969."""
+    app = ListOfNestedSelectorsApp()
+    red = Color.parse("red")
+    async with app.run_test():
+        assert app.query_one(".foo").styles.background == red
+        assert app.query_one(".bar").styles.background == red
+        assert app.query_one(".heh").styles.background != red
+
+
+class DeclarationAfterNestedApp(App[None]):
+    CSS = """
+    Screen {
+        Label {
+            background: red;
+        }
+        background: green;
+    }
+    """
+
+    def compose(self) -> ComposeResult:
+        yield Label("one")
+
+
+async def test_rule_declaration_after_nested() -> None:
+    """Regression test for https://github.com/Textualize/textual/issues/3999."""
+    app = DeclarationAfterNestedApp()
+    async with app.run_test():
+        assert app.screen.styles.background == Color.parse("green")
+        assert app.query_one(Label).styles.background == Color.parse("red")
+
+
 @pytest.mark.parametrize(
     ("css", "exception"),
     [