diff --git a/lib/sablon/html/ast.rb b/lib/sablon/html/ast.rb
index 5df9e2f..7c7a421 100644
--- a/lib/sablon/html/ast.rb
+++ b/lib/sablon/html/ast.rb
@@ -256,8 +256,16 @@ module Sablon
       end
 
       # moves any list tags that are a child of a list item tag up one level
-      # so they become a sibling instead of a child
+      # so they become a sibling instead of a child, and unwraps <p> tags
+      # directly inside <li> to prevent nested w:p elements in OOXML
       def process_child_nodes(node)
+        # Unwrap <p> directly inside <li>: editors such as Trix wrap list item
+        # text in <p>, but <li> already maps to w:p so nesting them produces
+        # invalid OOXML that Word silently drops.
+        node.xpath("./li/p").each do |p|
+          p.replace(p.children)
+        end
+
         node.xpath("./li/ul | ./li/ol").each do |list|
           # transfer attributes from parent now because the list tag will
           # no longer be a child and won't inheirit them as usual
diff --git a/test/html/ast_test.rb b/test/html/ast_test.rb
index 95e4003..c633524 100644
--- a/test/html/ast_test.rb
+++ b/test/html/ast_test.rb
@@ -133,6 +133,14 @@ class HTMLConverterASTTest < Sablon::TestCase
     assert_equal %w[0 0], get_numpr_prop_from_ast(ast, :ilvl)
   end
 
+  def test_p_inside_li_is_unwrapped
+    # editors like Trix wrap <li> text in <p>; the <p> must be unwrapped
+    # because <li> already maps to w:p and OOXML forbids nested w:p elements
+    input = '<ul><li><p>item one</p></li><li><p>item two</p></li></ul>'
+    ast = @converter.processed_ast(input)
+    assert_equal '<Root: [<List: [<Paragraph{ListBullet}: [<Run{}: item one>]>, <Paragraph{ListBullet}: [<Run{}: item two>]>]>]>', ast.inspect
+  end
+
   def test_table_tag
     input='<table></table>'
     ast = @converter.processed_ast(input)
