diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2192cf6d..ced639c84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,11 +23,12 @@ still making big changes.
 ### Enhancements
 
 - Ovals!
+- Lots more text methods: del, sub, sup; lots more text styles: underline, strikethrough, strikecolor, align
 - Features! Shoes.app(feature: [:html, :scarpe]) lets apps declare dependencies on non-classic Shoes!
+- Better handling of :left, :top, :width and :height, :margin and :padding on more drawables
 - The html_class style is a feature to make it easier to do Bootstrap styling on your drawables
 - Directly run Shoes Specs, including with Niente
 - We use Minitest assertion DSL rather than our own everywhere now
-- Better handling of :left, :top, :width and :height, :margin and :padding on more drawables
 
 ### Bugs Fixed
 
@@ -36,6 +37,7 @@ still making big changes.
 
 ### Incompatibilities
 
+TextDrawables now draw with very different Calzini (HTML renderer) properties
 We're deprecating the CatsCradle test DSL in favour of Shoes-Spec.
 Some error names have changed, with more to come.
 We've changed the Lacci drawable-create event to include the parent ID.
diff --git a/examples/span.rb b/examples/span.rb
index cd0bb98f3..1c841190f 100644
--- a/examples/span.rb
+++ b/examples/span.rb
@@ -1,6 +1,8 @@
 Shoes.app :height => 500, :width => 500 do
   stack :margin => 10 do
-    para span("TEXT EDITOR", :stroke => "blue", :fill => "green"), " * USE ALT-Q TO QUIT", :stroke => "red"
+    para span("TEXT EDITOR", :stroke => blue, :fill => green), " * USE ALT-Q TO QUIT", :stroke => red
   end
-  span ("text")
+  para "Various ", del("text"), " in ", sub("various"), " ", sup("styles"), " can be ", ins("hard to read"), "...\n"
+
+  para "A ", span("wide", underline: "single", undercolor: blue), " ", span("variety", underline: "error", undercolor: green), " ", span("of", underline: "double"), " ", span("underlines", underline: "low", undercolor: darkgreen)
 end
diff --git a/lacci/lib/shoes/drawables.rb b/lacci/lib/shoes/drawables.rb
index 375c04f99..951dc8195 100644
--- a/lacci/lib/shoes/drawables.rb
+++ b/lacci/lib/shoes/drawables.rb
@@ -27,6 +27,5 @@
 require "shoes/drawables/list_box"
 require "shoes/drawables/para"
 require "shoes/drawables/radio"
-require "shoes/drawables/span"
 require "shoes/drawables/video"
 require "shoes/drawables/progress"
diff --git a/lacci/lib/shoes/drawables/edit_box.rb b/lacci/lib/shoes/drawables/edit_box.rb
index 4f6f7c022..be562a994 100644
--- a/lacci/lib/shoes/drawables/edit_box.rb
+++ b/lacci/lib/shoes/drawables/edit_box.rb
@@ -24,7 +24,7 @@ def change(&block)
     end
 
     def append(new_text)
-      self.text = self.text + new_text
+      self.text = (self.text || "") + new_text
     end
   end
 end
diff --git a/lacci/lib/shoes/drawables/link.rb b/lacci/lib/shoes/drawables/link.rb
index 54226001a..f40e09ce6 100644
--- a/lacci/lib/shoes/drawables/link.rb
+++ b/lacci/lib/shoes/drawables/link.rb
@@ -5,10 +5,10 @@ class Link < Shoes::TextDrawable
     shoes_styles :text, :click, :has_block
     shoes_events :click
 
-    Shoes::Drawable.drawable_default_styles[Shoes::Link][:click] = "#"
+    #Shoes::Drawable.drawable_default_styles[Shoes::Link][:click] = "#"
 
-    init_args :text
-    def initialize(text, click: nil, &block)
+    init_args # Empty by the time it reaches Drawable#initialize
+    def initialize(*args, **kwargs, &block)
       @block = block
       # We can't send a block to the display drawable, but we can send a boolean
       @has_block = !block.nil?
@@ -18,8 +18,6 @@ def initialize(text, click: nil, &block)
       bind_self_event("click") do
         @block&.call
       end
-
-      create_display_drawable
     end
   end
 
@@ -27,7 +25,7 @@ def initialize(text, click: nil, &block)
   # hovered over. The functionality isn't present in Lacci yet.
   class LinkHover < Link
     def initialize
-      raise "This class should never be instantiated! Use link, not link_hover!"
+      raise "This class should never be instantiated directly! Use link, not link_hover!"
     end
   end
 end
diff --git a/lacci/lib/shoes/drawables/para.rb b/lacci/lib/shoes/drawables/para.rb
index 701ef7117..fb93c9232 100644
--- a/lacci/lib/shoes/drawables/para.rb
+++ b/lacci/lib/shoes/drawables/para.rb
@@ -3,7 +3,8 @@
 class Shoes
   class Para < Shoes::Drawable
     shoes_styles :text_items, :size, :font
-    shoes_style(:stroke) { |val| Shoes::Colors.to_rgb(val) }
+    shoes_style(:stroke) { |val, _name| Shoes::Colors.to_rgb(val) }
+    shoes_style(:fill) { |val, _name| Shoes::Colors.to_rgb(val) }
 
     shoes_style(:align) do |val|
       unless ["left", "center", "right"].include?(val)
@@ -51,7 +52,7 @@ def initialize(*args, **kwargs)
     private
 
     def text_children_to_items(text_children)
-      text_children.map { |arg| arg.is_a?(String) ? arg : arg.linkable_id }
+      text_children.map { |arg| arg.is_a?(TextDrawable) ? arg.linkable_id : arg.to_s }
     end
 
     public
@@ -159,7 +160,5 @@ def caption(*args, **kwargs)
     def inscription(*args, **kwargs)
       para(*args, **{ size: :inscription }.merge(kwargs))
     end
-
-    alias_method :ins, :inscription
   end
 end
diff --git a/lacci/lib/shoes/drawables/span.rb b/lacci/lib/shoes/drawables/span.rb
deleted file mode 100644
index cca5f001b..000000000
--- a/lacci/lib/shoes/drawables/span.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-class Shoes
-  class Span < Shoes::Drawable
-    shoes_styles :text, :stroke, :fill, :size, :font, :html_attributes
-    shoes_events # No Span-specific events yet
-
-    Shoes::Drawable.drawable_default_styles[Shoes::Span][:size] = :span
-
-    init_args
-    opt_init_args :text, :stroke, :size, :font
-    def initialize(*args, **html_attributes)
-      super
-
-      @html_attributes = html_attributes
-
-      create_display_drawable
-    end
-
-    def replace(text)
-      @text = text
-
-      # This should signal the display drawable to change
-      self.text = @text
-    end
-  end
-end
diff --git a/lacci/lib/shoes/drawables/text_drawable.rb b/lacci/lib/shoes/drawables/text_drawable.rb
index 36157bed6..844de9d70 100644
--- a/lacci/lib/shoes/drawables/text_drawable.rb
+++ b/lacci/lib/shoes/drawables/text_drawable.rb
@@ -9,23 +9,89 @@ class Shoes
   # have methods app, contents, children, parent,
   # style, to_s, text, text= and replace.
   #
-  # We don't currently allow things like em("oh", strong("hi!")),
-  # so we'll need a rework to match the old interface at
-  # some point.
+  # Much of what this does and how is similar to Para.
+  # It's a very similar API.
   class TextDrawable < Shoes::Drawable
-    class << self
-      # rubocop:disable Lint/MissingSuper
-      def inherited(subclass)
-        Shoes::Drawable.drawable_classes ||= []
-        Shoes::Drawable.drawable_classes << subclass
-
-        Shoes::Drawable.drawable_default_styles ||= {}
-        Shoes::Drawable.drawable_default_styles[subclass] = {}
+    shoes_styles :text_items, :size, :stroke, :strokewidth, :fill, :undercolor, :font
+
+    STRIKETHROUGH_VALUES = [nil, "none", "single"]
+    shoes_style :strikethrough do |val, _name|
+      unless STRIKETHROUGH_VALUES.include?(val)
+        raise Shoes::Errors::InvalidAttributeValueError, "Strikethrough must be one of: #{STRIKETHROUGH_VALUES.inspect}!"
+      end
+      val
+    end
+
+    UNDERLINE_VALUES = [nil, "none", "single", "double", "low", "error"]
+    shoes_style :underline do |val, _name|
+      unless UNDERLINE_VALUES.include?(val)
+        raise Shoes::Errors::InvalidAttributeValueError, "Underline must be one of: #{UNDERLINE_VALUES.inspect}!"
       end
-      # rubocop:enable Lint/MissingSuper
+      val
     end
 
     shoes_events # No TextDrawable-specific events yet
+
+    def initialize(*args, **kwargs)
+      # Don't pass text_children args to Drawable#initialize
+      super(*[], **kwargs)
+
+      # Text_children alternates strings and TextDrawables, so we can't just pass
+      # it as a Shoes style. It won't serialize.
+      update_text_children(args)
+
+      create_display_drawable
+    end
+
+    def text_children_to_items(text_children)
+      text_children.map { |arg| arg.is_a?(TextDrawable) ? arg.linkable_id : arg.to_s }
+    end
+
+    # Sets the paragraph text to a new value, which can
+    # include {TextDrawable}s like em(), strong(), etc.
+    #
+    # @param children [Array] the arguments can be Strings and/or TextDrawables
+    # @return [void]
+    def replace(*children)
+      update_text_children(children)
+    end
+
+    # Set the paragraph text to a single String.
+    # To use bold, italics, etc. use {Para#replace} instead.
+    #
+    # @param child [String] the new text to use for this Para
+    # @return [void]
+    def text=(*children)
+      update_text_children(children)
+    end
+
+    # Return the text, but not the styling, of the para's
+    # contents. For example, if the contents had strong
+    # and emphasized text, the bold and emphasized would
+    # be removed but the text would be returned.
+    #
+    # @return [String] the text from this para
+    def text
+      @text_children.map(&:to_s).join
+    end
+
+    # Return the text but not styling from the para. This
+    # is the same as #text.
+    #
+    # @return [String] the text from this para
+    def to_s
+      self.text
+    end
+
+    private
+
+    # Text_children alternates strings and TextDrawables, so we can't just pass
+    # it as a Shoes style. It won't serialize.
+    def update_text_children(children)
+      @text_children = children.flatten
+      # This should signal the display drawable to change
+      self.text_items = text_children_to_items(@text_children)
+    end
   end
 
   class << self
@@ -33,37 +99,24 @@ def default_text_drawable_with(element)
       class_name = element.capitalize
 
       drawable_class = Class.new(Shoes::TextDrawable) do
-        shoes_style :content
         shoes_events # No specific events
 
         init_args # We're going to pass an empty array to super
-        def initialize(content)
-          super()
-
-          @content = content
-
-          create_display_drawable
-        end
-
-        def text
-          self.content
-        end
-
-        def to_s
-          self.content
-        end
-
-        def text=(new_text)
-          self.content = new_text
-        end
       end
       Shoes.const_set class_name, drawable_class
     end
   end
 end
 
-# Shoes3 subclasses of cText were: code, del, em, ins, span, strong, sup, sub
-
 Shoes.default_text_drawable_with(:code)
+Shoes.default_text_drawable_with(:del)
 Shoes.default_text_drawable_with(:em)
 Shoes.default_text_drawable_with(:strong)
+Shoes.default_text_drawable_with(:span)
+Shoes.default_text_drawable_with(:sub)
+Shoes.default_text_drawable_with(:sup)
+Shoes.default_text_drawable_with(:ins) # in Shoes3, looks like "ins" is just underline
+
+# Defaults must come *after* classes are defined
+
+Shoes::Drawable.drawable_default_styles[Shoes::Ins][:underline] = "single"
diff --git a/lib/scarpe/wv.rb b/lib/scarpe/wv.rb
index 9f9441194..834b3e2b5 100644
--- a/lib/scarpe/wv.rb
+++ b/lib/scarpe/wv.rb
@@ -95,7 +95,6 @@ class Scarpe::Webview::Drawable < Shoes::Linkable
 require_relative "wv/shape"
 
 require_relative "wv/text_drawable"
-require_relative "wv/span"
 require_relative "wv/link"
 require_relative "wv/line"
 require_relative "wv/rect"
diff --git a/lib/scarpe/wv/document_root.rb b/lib/scarpe/wv/document_root.rb
index 3bbe739ec..d6aab29b3 100644
--- a/lib/scarpe/wv/document_root.rb
+++ b/lib/scarpe/wv/document_root.rb
@@ -16,12 +16,12 @@ def initialize(properties)
         when "font"
           @fonts << args[0]
           # Can't just create font_updater and alert_updater on initialize - not everything is set up
-          @font_updater ||= Scarpe::Webview::WebWrangler::ElementWrangler.new("root-fonts")
+          @font_updater ||= Scarpe::Webview::WebWrangler::ElementWrangler.new(html_id: "root-fonts")
           @font_updater.inner_html = font_contents
         when "alert"
           bind_ok_event
           @alerts << args[0]
-          @alert_updater ||= Scarpe::Webview::WebWrangler::ElementWrangler.new("root-alerts")
+          @alert_updater ||= Scarpe::Webview::WebWrangler::ElementWrangler.new(html_id: "root-alerts")
           @alert_updater.inner_html = alert_contents
         else
           raise Scarpe::UnknownBuiltinCommandError, "Unexpected builtin command: #{cmd_name.inspect}!"
diff --git a/lib/scarpe/wv/drawable.rb b/lib/scarpe/wv/drawable.rb
index 1425f0119..dc3a968cd 100644
--- a/lib/scarpe/wv/drawable.rb
+++ b/lib/scarpe/wv/drawable.rb
@@ -153,7 +153,7 @@ def add_child(child)
     #
     # @return [Scarpe::WebWrangler::ElementWrangler] a DOM object manager
     def html_element
-      @elt_wrangler ||= Scarpe::Webview::WebWrangler::ElementWrangler.new(html_id)
+      @elt_wrangler ||= Scarpe::Webview::WebWrangler::ElementWrangler.new(html_id:)
     end
 
     # Return a promise that guarantees all currently-requested changes have completed
diff --git a/lib/scarpe/wv/link.rb b/lib/scarpe/wv/link.rb
index 4fa41c5fa..96ffe89e2 100644
--- a/lib/scarpe/wv/link.rb
+++ b/lib/scarpe/wv/link.rb
@@ -10,8 +10,10 @@ def initialize(properties)
       end
     end
 
-    def element
-      render "link"
+    def to_calzini_hash
+      h = super
+      h[:tag] = "a"
+      h
     end
   end
 end
diff --git a/lib/scarpe/wv/para.rb b/lib/scarpe/wv/para.rb
index 1ecf499e7..f10317094 100644
--- a/lib/scarpe/wv/para.rb
+++ b/lib/scarpe/wv/para.rb
@@ -61,6 +61,7 @@ def to_html
     private
 
     def child_markup
+      # The children should be only text strings or TextDrawables.
       items_to_display_children(@text_items).map do |child|
         if child.respond_to?(:to_html)
           child.to_html
diff --git a/lib/scarpe/wv/span.rb b/lib/scarpe/wv/span.rb
deleted file mode 100644
index 0a8011672..000000000
--- a/lib/scarpe/wv/span.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-module Scarpe::Webview
-  class Span < TextDrawable
-    SIZES = {
-      inscription: 10,
-      ins: 10,
-      span: 12,
-      caption: 14,
-      tagline: 18,
-      subtitle: 26,
-      title: 34,
-      banner: 48,
-    }.freeze
-    private_constant :SIZES
-
-    def initialize(properties)
-      super
-    end
-
-    def properties_changed(changes)
-      text = changes.delete("text")
-      if text
-        html_element.inner_html = text
-        return
-      end
-
-      # Not deleting, so this will re-render
-      if changes["size"] && SIZES[@size.to_sym]
-        @size = @size.to_sym
-      end
-
-      super
-    end
-
-    def element(&block)
-      render("span", &block)
-    end
-
-    def to_html
-      element { @text }
-    end
-  end
-end
diff --git a/lib/scarpe/wv/text_drawable.rb b/lib/scarpe/wv/text_drawable.rb
index 946a57f48..60a1843d2 100644
--- a/lib/scarpe/wv/text_drawable.rb
+++ b/lib/scarpe/wv/text_drawable.rb
@@ -1,32 +1,90 @@
 # frozen_string_literal: true
 
+# There are different ways to implement these tags. You can change the HTML tag (link,
+# code, strong) or set default values (del for strikethrough.) There's no reason the
+# Shoes tag name has to match the HTML tag (del, link). This can be a little
+# complicated since CSS often sets default values (e.g. del for strikethrough) and
+# Scarpe may use those default values or override them. Long term it may be easier
+# for us to set up our own CSS for this somehow that does *not* use the HTML-tag
+# defaults since the browser can mess with those, and there's no guarantee that
+# Webview uses the same default CSS style across all OSes.
+
 module Scarpe::Webview
+  # This class renders text tags like em, strong, link, etc.
   class TextDrawable < Drawable
-    def to_html
-      # Do not render TextDrawables with individual wrapper divs.
-      element
+    # Calzini renders based on properties, mostly Shoes styles.
+    # To have Calzini render this for us, we convert to the format
+    # Calzini expects and then let it render. See Webview::Para
+    # for the specific Calzini call.
+    def to_calzini_hash
+      text_array = items_to_display_children(@text_items).map do |item|
+        if item.respond_to?(:to_calzini_hash)
+          item.to_calzini_hash
+        elsif item.is_a?(String)
+          item
+        else
+          # This should normally be filtered out in Lacci, long before we see it
+          raise "Unrecognized item in TextDrawable! #{item.inspect}"
+        end
+      end
+
+      {
+        items: text_array,
+        html_id: @linkable_id.to_s,
+        tag: nil, # have Calzini assign a default unless a subclass overrides this
+        props: shoes_styles,
+      }
+    end
+
+    def element
+      render("text_drawable", [to_calzini_hash])
+    end
+
+    def items_to_display_children(items)
+      return [] if items.nil?
+
+      items.map do |item|
+        if item.is_a?(String)
+          item
+        else
+          Scarpe::Webview::DisplayService.instance.query_display_drawable_for(item)
+        end
+      end
+    end
+
+    # Usually we query by ID, but for TextDrawable it has to be by class.
+    # That's how needs_update!, etc continue to work.
+    def html_element
+      @elt_wrangler ||= Scarpe::Webview::WebWrangler::ElementWrangler.new(selector: %{document.getElementsByClassName("id_#{html_id}")}, multi: true)
     end
   end
 
   class << self
-    def default_wv_text_drawable_with(element)
-      webview_class_name = element.capitalize
+    def default_wv_text_drawable_with_tag(shoes_tag, html_tag = nil)
+      html_tag ||= shoes_tag
+      webview_class_name = shoes_tag.capitalize
       webview_drawable_class = Class.new(Scarpe::Webview::TextDrawable) do
-        def initialize(properties)
-          class_name = self.class.name.split("::")[-1]
-          @html_tag = class_name.delete_prefix("Webview").downcase
-          super
+        class << self
+          attr_accessor :html_tag
         end
 
-        def element
-          render(@html_tag) { @content.to_s }
+        def to_calzini_hash
+          h = super
+          h[:tag] = self.class.html_tag
+          h
         end
       end
       Scarpe::Webview.const_set webview_class_name, webview_drawable_class
+      webview_drawable_class.html_tag = html_tag
     end
   end
 end
 
-Scarpe::Webview.default_wv_text_drawable_with(:code)
-Scarpe::Webview.default_wv_text_drawable_with(:em)
-Scarpe::Webview.default_wv_text_drawable_with(:strong)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:code)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:del)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:em)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:strong)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:span)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:sub)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:sup)
+Scarpe::Webview.default_wv_text_drawable_with_tag(:ins, "span") # Styled in Shoes, not CSS
diff --git a/lib/scarpe/wv/web_wrangler.rb b/lib/scarpe/wv/web_wrangler.rb
index 33e704630..1ad576048 100644
--- a/lib/scarpe/wv/web_wrangler.rb
+++ b/lib/scarpe/wv/web_wrangler.rb
@@ -747,16 +747,37 @@ def schedule_waiting_changes
   class ElementWrangler
     attr_reader :html_id
 
-    # Create an ElementWrangler for the given HTML ID
+    # Create an ElementWrangler for the given HTML ID or selector.
+    # The caller should provide exactly one of the html_id or selector.
     #
     # @param html_id [String] the HTML ID for the DOM element
-    def initialize(html_id)
+    def initialize(html_id: nil, selector: nil, multi: false)
       @webwrangler = ::Scarpe::Webview::DisplayService.instance.wrangler
       raise Scarpe::MissingWranglerError, "Can't get WebWrangler!" unless @webwrangler
 
-      @html_id = html_id
+      if html_id && !selector
+        @selector = "document.getElementById('" + html_id + "')"
+      elsif selector && !html_id
+        @selector = selector
+      else
+        raise ArgumentError, "Must provide exactly one of html_id or selector!"
+      end
+
+      @multi = multi
     end
 
+    private
+
+    def on_each(fragment)
+      if @multi
+        @webwrangler.dom_change("a = Array.from(#{@selector}); a.forEach((item) => item#{fragment}); true")
+      else
+        @webwrangler.dom_change(@selector + fragment + ";true")
+      end
+    end
+
+    public
+
     # Return a promise that will be fulfilled when all changes scheduled via
     # this ElementWrangler are verified complete.
     #
@@ -770,7 +791,7 @@ def promise_update
     # @param new_value [String] the new value
     # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
     def value=(new_value)
-      @webwrangler.dom_change("document.getElementById('" + html_id + "').value = `" + new_value + "`; true")
+      on_each(".value = `" + new_value + "`")
     end
 
     # Update the JS DOM element's inner_text. The given Ruby value will be converted to string and assigned in single-quotes.
@@ -778,7 +799,7 @@ def value=(new_value)
     # @param new_text [String] the new inner_text
     # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
     def inner_text=(new_text)
-      @webwrangler.dom_change("document.getElementById('" + html_id + "').innerText = '" + new_text + "'; true")
+      on_each(".innerText = '" + new_text + "'")
     end
 
     # Update the JS DOM element's inner_html. The given Ruby value will be converted to string and assigned in backquotes.
@@ -786,7 +807,7 @@ def inner_text=(new_text)
     # @param new_html [String] the new inner_html
     # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
     def inner_html=(new_html)
-      @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").innerHTML = `" + new_html + "`; true")
+      on_each(".innerHTML = `" + new_html + "`")
     end
 
     # Update the JS DOM element's outer_html. The given Ruby value will be converted to string and assigned in backquotes.
@@ -794,7 +815,7 @@ def inner_html=(new_html)
     # @param new_html [String] the new outer_html
     # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
     def outer_html=(new_html)
-      @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").outerHTML = `" + new_html + "`; true")
+      on_each(".outerHTML = `" + new_html + "`")
     end
 
     # Update the JS DOM element's attribute. The given Ruby value will be inspected and assigned.
@@ -803,7 +824,7 @@ def outer_html=(new_html)
     # @param value [String] the new attribute value
     # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
     def set_attribute(attribute, value)
-      @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").setAttribute(" + attribute.inspect + "," + value.inspect + "); true")
+      on_each(".setAttribute(" + attribute.inspect + "," + value.inspect + ")")
     end
 
     # Update an attribute of the JS DOM element's style. The given Ruby value will be inspected and assigned.
@@ -812,19 +833,19 @@ def set_attribute(attribute, value)
     # @param value [String] the new style attribute value
     # @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
     def set_style(style_attr, value)
-      @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").style.#{style_attr} = " + value.inspect + "; true")
+      on_each(".style.#{style_attr} = " + value.inspect + ";")
     end
 
     # Remove the specified DOM element
     #
     # @return [Scarpe::Promise] a promise that wil be fulfilled when the element is removed
     def remove
-      @webwrangler.dom_change("document.getElementById('" + html_id + "').remove(); true")
+      on_each(".remove()")
     end
 
     def toggle_input_button(mark)
       checked_value = mark ? "true" : "false"
-      @webwrangler.dom_change("document.getElementById('#{html_id}').checked = #{checked_value};")
+      on_each(".checked = #{checked_value}")
     end
   end
 end
diff --git a/lib/scarpe/wv/webview_local_display.rb b/lib/scarpe/wv/webview_local_display.rb
index 285521fef..7a7ec8f00 100644
--- a/lib/scarpe/wv/webview_local_display.rb
+++ b/lib/scarpe/wv/webview_local_display.rb
@@ -55,7 +55,7 @@ def initialize
     def create_display_drawable_for(drawable_class_name, drawable_id, properties, parent_id:, is_widget:)
       existing = query_display_drawable_for(drawable_id, nil_ok: true)
       if existing
-        @log.warn("There is already a display drawable for #{drawable_id.inspect}! Returning #{existing.class.name}.")
+        @log.warn("There is already a Scarpe drawable for #{drawable_id.inspect}! Returning #{existing.class.name} rather than creating a #{drawable_class_name}.")
         return existing
       end
 
diff --git a/scarpe-components/lib/scarpe/components/calzini/art_widgets.rb b/scarpe-components/lib/scarpe/components/calzini/art_drawables.rb
similarity index 100%
rename from scarpe-components/lib/scarpe/components/calzini/art_widgets.rb
rename to scarpe-components/lib/scarpe/components/calzini/art_drawables.rb
diff --git a/scarpe-components/lib/scarpe/components/calzini/para.rb b/scarpe-components/lib/scarpe/components/calzini/para.rb
index 586bc6cc7..321763131 100644
--- a/scarpe-components/lib/scarpe/components/calzini/para.rb
+++ b/scarpe-components/lib/scarpe/components/calzini/para.rb
@@ -1,17 +1,24 @@
 # frozen_string_literal: true
 
 module Scarpe::Components::Calzini
-  # para_element is a bit of a hard one, since it does not-entirely-trivial
-  # mapping between display objects and IDs. But we don't want Calzini
-  # messing with the display service or display objects.
   def para_element(props, &block)
+    # Align requires an extra wrapping div.
+
+    # Stacking strikethrough with underline requires multiple elements.
+    # We handle this by making strikethrough part of the main element,
+    # but using an extra wrapping element for underline.
+
+    tag = props["tag"] || "p"
+
+    para_styles, extra_styles = para_style(props)
+
     HTML.render do |h|
-      if props["align"]
-        h.div(id: html_id, style: {"text-align": props["align"], width: "100%"}) do
-          h.p(style: para_style(props), &block)
-        end
+      if extra_styles.empty?
+        h.send(tag, id: html_id, style: para_styles, &block)
       else
-        h.p(id: html_id, style: para_style(props), &block)
+        h.div(id: html_id, style: extra_styles.merge(width: "100%")) do
+          h.send(tag, style: para_styles, &block)
+        end
       end
     end
   end
@@ -19,11 +26,57 @@ def para_element(props, &block)
   private
 
   def para_style(props)
-    drawable_style(props).merge({
-      color: rgb_to_hex(props["stroke"]),
+    ds = drawable_style(props)
+    s1, s2 = text_specific_styles(props)
+    [ds.merge(s1), s2]
+  end
+
+  def text_specific_styles(props)
+    # Shoes3 allows align: right on TextDrawables like em(), but it does
+    # nothing. We can ignore it or (maybe in future?) warn if we see it.
+
+    strikethrough = props["strikethrough"]
+    strikethrough = nil if strikethrough == "" || strikethrough == "none"
+    s1 = {
+      "color": rgb_to_hex(props["stroke"]),
+      "background-color": rgb_to_hex(props["fill"]),
       "font-size": para_font_size(props),
       "font-family": props["font"],
-    }.compact)
+      "text-decoration-line": strikethrough ? "line-through" : nil,
+      "text-decoration-color": props["strikecolor"] ? rgb_to_hex(props["strikecolor"]) : nil,
+    }.compact
+
+    s2 = {}
+    if props["align"] && props["align"] != ""
+      s2[:"text-align"] = props["align"]
+    end
+
+    unless [nil, "none"].include?(props["underline"])
+      undercolor = rgb_to_hex props["undercolor"]
+      s2["text-decoration-color"] = undercolor if undercolor
+    end
+
+    # [nil, "none", "single", "double", "low", "error"]
+    case props["underline"]
+    when nil, "none"
+      # Do nothing
+    when "single"
+      s2["text-decoration-line"] = "underline"
+    when "double"
+      s2["text-decoration-line"] = "underline"
+      s2["text-decoration-style"] = "double"
+    when "error"
+      s2["text-decoration-line"] = "underline"
+      s2["text-decoration-style"] = "wavy"
+    when "low"
+      s2["text-decoration-line"] = "underline"
+      s2["text-underline-offset"] = "0.3rem"
+    else
+      # This should normally be unreachable
+      raise Shoes::Errors::InvalidAttributeValueError, "Unexpected underline type #{props["underline"].inspect}!"
+    end
+
+    [s1, s2]
   end
 
   def para_font_size(props)
@@ -34,4 +87,67 @@ def para_font_size(props)
 
     dimensions_length(font_size)
   end
+
+  public
+
+  # The text element is used to render the equivalent of Shoes cText,
+  # which includes em, strong, span, link and so on. We use a
+  # "content" tag for it which alternates plaintext with a hash of
+  # properties.
+  def text_drawable_element(prop_array)
+    out = String.new # Need unfrozen string
+
+    # Each item should be a String or a property Hash
+    # :items, :html_id, :tag, :props
+    prop_array.each do |item|
+      if item.is_a?(String)
+        out << item.gsub("\n", "<br/>")
+      else
+        s, extra = text_drawable_style(item[:props])
+        out << HTML.render do |h|
+          if extra.empty?
+            h.send(
+              item[:tag] || "span",
+              class: "id_#{item[:html_id]}",
+              style: s,
+              **text_drawable_attrs(item[:props])
+            ) do
+              text_drawable_element(item[:items])
+            end
+          else
+            h.span(class: "id_#{item[:html_id]}", style: extra) do
+              h.send(
+                item[:tag] || "span",
+                class: "id_#{item[:html_id]}",
+                style: s,
+                **text_drawable_attrs(item[:props])
+              ) do
+                text_drawable_element(item[:items])
+              end
+            end
+          end
+        end
+      end
+    end
+
+    out
+  end
+
+  private
+
+  def text_drawable_attrs(props)
+    {
+      # These properties will normally only be set by link()
+      href: props["click"],
+      onclick: props["has_block"] ? handler_js_code("click") : nil,
+    }.compact
+  end
+
+  def text_drawable_style(props)
+    s, extra_s = text_specific_styles(props)
+
+    # Add hover styles, perhaps with CSS pseudo-class
+
+    [drawable_style(props).merge(s), extra_s]
+  end
 end
diff --git a/scarpe-components/lib/scarpe/components/calzini/text_widgets.rb b/scarpe-components/lib/scarpe/components/calzini/text_widgets.rb
deleted file mode 100644
index 4dc55de00..000000000
--- a/scarpe-components/lib/scarpe/components/calzini/text_widgets.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-module Scarpe::Components::Calzini
-  def link_element(props)
-    HTML.render do |h|
-      h.a(**link_attributes(props)) do
-        props["text"]
-      end
-    end
-  end
-
-  def span_element(props, &block)
-    HTML.render do |h|
-      h.span(**span_options(props), &block)
-    end
-  end
-
-  def code_element(props, &block)
-    HTML.render do |h|
-      h.code(&block)
-    end
-  end
-
-  def em_element(props, &block)
-    HTML.render do |h|
-      h.em(&block)
-    end
-  end
-
-  def strong_element(props, &block)
-    HTML.render do |h|
-      h.strong(&block)
-    end
-  end
-
-  private
-
-  def link_attributes(props)
-    {
-      id: html_id,
-      href: props["click"],
-      onclick: (handler_js_code("click") if props["has_block"]),
-      style: drawable_style(props),
-    }.compact
-  end
-
-  def span_style(props)
-    {
-      color: props["stroke"],
-      "font-size": span_font_size(props),
-      "font-family": props["font"],
-    }.compact
-  end
-
-  def span_options(props)
-    { id: html_id, style: span_style(props) }
-  end
-
-  def span_font_size(props)
-    sz = props["size"]
-    font_size = SIZES.key?(sz.to_s.to_sym) ? SIZES[sz.to_s.to_sym] : sz
-
-    dimensions_length(font_size)
-  end
-end
diff --git a/scarpe-components/lib/scarpe/components/html.rb b/scarpe-components/lib/scarpe/components/html.rb
index e05643ee2..221713aa0 100644
--- a/scarpe-components/lib/scarpe/components/html.rb
+++ b/scarpe-components/lib/scarpe/components/html.rb
@@ -20,6 +20,9 @@ class Scarpe::Components::HTML
     :u,
     :line,
     :span,
+    :sub,
+    :sup,
+    :del,
     :svg,
     :h1,
     :h2,
diff --git a/scarpe-components/lib/scarpe/components/tiranti.rb b/scarpe-components/lib/scarpe/components/tiranti.rb
index d619bea1f..042aab9fc 100644
--- a/scarpe-components/lib/scarpe/components/tiranti.rb
+++ b/scarpe-components/lib/scarpe/components/tiranti.rb
@@ -145,58 +145,23 @@ def progress_element(props)
     end
   end
 
-  # para_element is a bit of a hard one, since it does not-entirely-trivial
-  # mapping between display objects and IDs. But we don't want Calzini
-  # messing with the display service or display objects.
   def para_element(props, &block)
-    tag, opts = para_elt_and_opts(props)
-
-    HTML.render do |h|
-      h.send(tag, **opts, &block)
-    end
-  end
-
-  private
-
-  ELT_AND_SIZE = {
-    inscription: [:p, 10],
-    ins: [:p, 10],
-    para: [:p, 12],
-    caption: [:p, 14],
-    tagline: [:p, 18],
-    subtitle: [:h3, 26],
-    title: [:h2, 34],
-    banner: [:h1, 48],
-  }.freeze
-
-  def para_elt_and_opts(props)
-    elt, size = para_elt_and_size(props)
-    size = dimensions_length(size)
-
-    para_style = drawable_style(props).merge({
-      color: rgb_to_hex(props["stroke"]),
-      "font-size": size,
-      "font-family": props["font"],
-    }.compact)
-
-    opts = (props["html_attributes"] || {}).merge(id: html_id, style: para_style)
-
-    [elt, opts]
-  end
-
-  def para_elt_and_size(props)
-    return [:p, nil] unless props["size"]
-
-    ps = props["size"].to_s.to_sym
-    if ELT_AND_SIZE.key?(ps)
-      ELT_AND_SIZE[ps]
+    ps, _extra = para_style(props)
+    size = ps[:"font-size"] || "12px"
+    size_int = size.to_i # Mostly useful if it's something like "12px"
+    if size.include?("calc") || size.end_with?("%")
+      # Very big text!
+      props["tag"] = "h2"
+    elsif size_int >= 48
+      props["tag"] = "h1"
+    elsif size_int >= 34
+      props["tag"] = "h2"
+    elsif size_int >= 26
+      props["tag"] = "h3"
     else
-      sz = props["size"].to_i
-      if sz > 18
-        [:h2, sz]
-      else
-        [:p, sz]
-      end
+      props["tag"] = "p"
     end
+
+    super
   end
 end
diff --git a/scarpe-components/test/calzini/test_calzini_para.rb b/scarpe-components/test/calzini/test_calzini_para.rb
index ccf7299de..e9c16cae2 100644
--- a/scarpe-components/test/calzini/test_calzini_para.rb
+++ b/scarpe-components/test/calzini/test_calzini_para.rb
@@ -7,7 +7,6 @@ def setup
     @calzini = CalziniRenderer.new
   end
 
-  # Note that Calzini doesn't render the text items for itself.
   def test_para_simple
     assert_equal %{<p id="elt-1">OK</p>},
       @calzini.render("para", {}) { "OK" }
@@ -32,11 +31,4 @@ def test_para_with_symbol_banner
     assert_equal %{<p id="elt-1" style="font-size:48px"></p>},
       @calzini.render("para", { "size" => :banner })
   end
-
-  # Eventually this should probably need to be marked as a Scarpe extension, here or
-  # elsewhere.
-  #def test_para_with_html_attributes
-  #  assert_equal %{<p avocado="true" class="avocado_bearing" id="elt-1"></p>},
-  #    @calzini.render("para", { "html_attributes" => { "avocado" => true, "class" => "avocado_bearing" } })
-  #end
 end
diff --git a/scarpe-components/test/calzini/test_calzini_text_drawables.rb b/scarpe-components/test/calzini/test_calzini_text_drawables.rb
index 8df7bb019..12f3582bf 100644
--- a/scarpe-components/test/calzini/test_calzini_text_drawables.rb
+++ b/scarpe-components/test/calzini/test_calzini_text_drawables.rb
@@ -7,30 +7,100 @@ def setup
     @calzini = CalziniRenderer.new
   end
 
-  def test_link_simple
-    assert_equal %{<a id="elt-1" href="https://google.com">click here</a>},
-      @calzini.render("link", { "click" => "https://google.com", "text" => "click here" })
+  def trim_html_ids(s)
+    s.gsub(/ class="id_\d+"/, "")
   end
 
-  def test_link_block
-    assert_equal %{<a id="elt-1" onclick="handle('click')">click here</a>},
-      @calzini.render("link", { "has_block" => true, "text" => "click here" })
+  def test_text_only_drawable
+    assert_equal %{this is text},
+      @calzini.render("text_drawable", ["this ", "is", " text"])
   end
 
-  def test_span_simple
-    assert_equal %{<span id="elt-1" style="color:red;font-size:48px;font-family:Lucida">big red</span>},
-      @calzini.render("span", { "stroke" => "red", "size" => :banner, "font" => "Lucida" }) { "big red" }
+  def test_simple_text_drawable_with_em
+    assert_equal %{this <em class="id_1">is</em> text},
+      @calzini.render("text_drawable",
+        ["this ", { tag: "em", html_id: "1", items: ["is"], props: {}}, " text"])
   end
 
-  def test_code_simple
-    assert_equal %{<code>Hello</code>}, @calzini.render("code", {}) { "Hello" }
+  # Span doesn't have default properties, so it's good for testing how a property is rendered
+  def test_simple_text_drawable_with_span_styles
+    assert_equal %{this <span style="color:#FF00FF;background-color:#0000FF;font-size:13px;font-family:Lucida">is</span> text},
+      trim_html_ids(@calzini.render("text_drawable",
+        ["this ", {
+          tag: "span",
+          html_id: "1",
+          items: ["is"],
+          props: {
+            "font" => "Lucida",
+            "size" => 13,
+            "stroke" => "#FF00FF",
+            "fill" => "#0000FF"
+          }
+        }, " text"]))
   end
 
-  def test_em_simple
-    assert_equal %{<em>Hello</em>}, @calzini.render("em", {}) { "Hello" }
+  def test_link_with_has_block
+    assert_equal %{this <a onclick="handle('click')">is</a> text},
+      trim_html_ids(@calzini.render("text_drawable",
+        ["this ", {
+          tag: "a",
+          html_id: "1",
+          items: ["is"],
+          props: {
+            "has_block" => true,
+          }
+        }, " text"]))
   end
 
-  def test_strong_simple
-    assert_equal %{<strong>Hello</strong>}, @calzini.render("strong", {}) { "Hello" }
+  def test_link_with_click
+    assert_equal %{this <a href="#" onclick="handle('click')">is</a> text},
+      trim_html_ids(@calzini.render("text_drawable",
+        ["this ", {
+          tag: "a",
+          html_id: "1",
+          items: ["is"],
+          props: {
+            "has_block" => true,
+            "click" => "#",
+          }
+        }, " text"]))
+  end
+
+  def test_del_tag
+    assert_equal %{this <del>is</del> text},
+      trim_html_ids(@calzini.render("text_drawable",
+        ["this ", {
+          tag: "del",
+          html_id: "1",
+          items: ["is"],
+          props: {
+          }
+        }, " text"]))
+  end
+
+  def test_single_strikethrough
+    assert_equal %{this <span style="text-decoration-line:line-through">is</span> text},
+      trim_html_ids(@calzini.render("text_drawable",
+        ["this ", {
+          tag: "span",
+          html_id: "1",
+          items: ["is"],
+          props: {
+            "strikethrough" => "single",
+          }
+        }, " text"]))
+  end
+
+  def test_single_strikethrough_none
+    assert_equal %{this <span>is</span> text},
+      trim_html_ids(@calzini.render("text_drawable",
+        ["this ", {
+          tag: "span",
+          html_id: "1",
+          items: ["is"],
+          props: {
+            "strikethrough" => "none",
+          }
+        }, " text"]))
   end
 end
diff --git a/test/shoes_spec_helper.rb b/test/shoes_spec_helper.rb
new file mode 100644
index 000000000..982183645
--- /dev/null
+++ b/test/shoes_spec_helper.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# This is intended to be required from inside a Shoes-Spec test, which allows writing test
+# helper code for them.
+
+module TextDrawableHelper
+  def trim_html_ids(s)
+    s.gsub(/ class="id_\d+"/, "")
+  end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index d8b7f9ab7..ab00c90c9 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -47,7 +47,8 @@ def run_scarpe_sspec(
     expect_assertions_min: nil,
     expect_assertions_max: nil,
     expect_result: :success,
-    display_service: "wv_local"
+    display_service: "wv_local",
+    html_renderer: "calzini"
   )
     test_output = File.expand_path(File.join __dir__, "sspec.json")
     test_method_name = self.name
@@ -58,6 +59,7 @@ def run_scarpe_sspec(
       # For unit testing always supply --debug so we get the most logging
       cmd = \
         "SCARPE_DISPLAY_SERVICE=#{display_service} " +
+        "SCARPE_HTML_RENDERER=#{html_renderer} " +
         "SCARPE_LOG_CONFIG=\"#{scarpe_log_config}\" " +
         "SHOES_MINITEST_EXPORT_FILE=\"#{test_output}\" " +
         "SHOES_MINITEST_CLASS_NAME=\"#{test_class_name}\" " +
diff --git a/test/test_link.rb b/test/test_link.rb
deleted file mode 100644
index 1079a7620..000000000
--- a/test/test_link.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-
-class TestWebviewLink < ScarpeTest
-  def setup
-    @default_properties = {
-      "text" => "click here",
-      "click" => "#",
-      "has_block" => false,
-      "shoes_linkable_id" => 1,
-    }
-  end
-
-  def with_mocked_binding(&block)
-    @mocked_disp_service = Minitest::Mock.new
-    @mocked_app = Minitest::Mock.new
-    @mocked_disp_service.expect(:app, @mocked_app)
-    @mocked_app.expect(:bind, nil, [String])
-
-    Scarpe::Webview::DisplayService.stub(:instance, @mocked_disp_service, &block)
-
-    @mocked_disp_service.verify
-    @mocked_app.verify
-  end
-
-  def test_link_with_url
-    with_mocked_binding do
-      link = Scarpe::Webview::Link.new(@default_properties.merge("click" => "https://www.google.com"))
-
-      assert_html link.to_html, :a, id: link.html_id, href: "https://www.google.com" do
-        "click here"
-      end
-    end
-  end
-
-  def test_link_with_block
-    with_mocked_binding do
-      link = Scarpe::Webview::Link.new(@default_properties.merge("has_block" => true))
-
-      assert_html link.to_html, :a, id: link.html_id, href: "#", onclick: link.handler_js_code("click") do
-        "click here"
-      end
-    end
-  end
-end
diff --git a/test/test_para.rb b/test/test_para.rb
index ac67c8775..21f8a192b 100644
--- a/test/test_para.rb
+++ b/test/test_para.rb
@@ -172,7 +172,7 @@ def test_para_stacking_text_drawables
         @p = para "Yo ", [em("EmphaYo "), strong("StrongYo")], em("empha"), ", plain"
       end
     SCARPE_APP
-      assert_includes para().display.to_html, "Yo <em>EmphaYo </em><strong>StrongYo</strong><em>empha</em>, plain"
+      assert_includes para().display.to_html, "Yo <em class=\"id_3\">EmphaYo </em><strong class=\"id_4\">StrongYo</strong><em class=\"id_5\">empha</em>, plain"
     TEST_CODE
   end
 end
diff --git a/test/test_sspec_infrastructure.rb b/test/test_sspec_infrastructure.rb
index 5dac276fd..83d8963cd 100644
--- a/test/test_sspec_infrastructure.rb
+++ b/test/test_sspec_infrastructure.rb
@@ -61,5 +61,4 @@ def test_assertion_fail
       assert_equal true, false, "This should always fail!"
     SSPEC
   end
-
 end
diff --git a/test/test_text_drawables.rb b/test/test_text_drawables.rb
new file mode 100644
index 000000000..c1bee23ee
--- /dev/null
+++ b/test/test_text_drawables.rb
@@ -0,0 +1,288 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class TestTextDrawables < ShoesSpecLoggedTest
+  self.logger_dir = File.expand_path("#{__dir__}/../logger")
+
+  def require_helper
+    "require " + File.expand_path("#{__dir__}/shoes_spec_helper.rb").inspect
+  end
+
+  # To test:
+  #
+  # * LinkHover
+
+  def test_basic_text_drawables
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is ", em("emphatically"), " ", strong("strongly"), " made of text!"
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_equal "This is emphatically strongly made of text!", para.text
+      assert_includes trim_html_ids(para.display.to_html), "This is <em>emphatically</em> <strong>strongly</strong> made of text!"
+    SSPEC
+  end
+
+  def test_para_stroke_and_fill
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is text", stroke: green, fill: blue
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      h = trim_html_ids(para.display.to_html)
+      assert_includes h, "This is text"
+      assert_includes h, 'color:#008000'
+      assert_includes h, 'background-color:#0000FF'
+    SSPEC
+  end
+
+  def test_styled_text_drawables
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is ", em("emphatically ", hidden: true), "somewhat hidden!"
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_equal "This is emphatically somewhat hidden!", para.text
+      assert_includes trim_html_ids(para.display.to_html), \%{This is <em style="display:none">emphatically </em>somewhat hidden!}
+    SSPEC
+  end
+
+  def test_basic_nested_text_drawables_1
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is ", em(strong("emphatically strongly")), " made of text!"
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_equal "This is emphatically strongly made of text!", para.text
+      assert_includes trim_html_ids(para.display.to_html), "This is <em><strong>emphatically strongly</strong></em> made of text!"
+    SSPEC
+  end
+
+  def test_basic_nested_text_drawables_2
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is ", em("emphatically and ", strong("empha-strongly")), " made of text!"
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_equal "This is emphatically and empha-strongly made of text!", para.text
+      assert_includes trim_html_ids(para.display.to_html), "This is <em>emphatically and <strong>empha-strongly</strong></em> made of text!"
+    SSPEC
+  end
+
+  def test_text_drawable_in_multiple_paras
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        t = em("emphatically")
+        @p1 = para "This is ", t, " ", strong("strongly"), " made of text!"
+        @p2 = para "And ", t, " a great idea!"
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_includes trim_html_ids(para("@p1").display.to_html), "This is <em>emphatically</em> <strong>strongly</strong> made of text!"
+      assert_includes trim_html_ids(para("@p2").display.to_html), "And <em>emphatically</em> a great idea!"
+    SSPEC
+  end
+
+  def test_text_drawable_multiple_para_update
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        @t = em("emphatically")
+        @p1 = para "This is ", @t, " ", strong("strongly"), " made of text!"
+        @p2 = para "And ", @t, " a great idea!"
+
+        button "Change" do
+          @t.text = "totally ", strong("unquestionably")
+        end
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      button.trigger_click
+
+      # Check to_html updating
+      assert_includes trim_html_ids(para("@p1").display.to_html), "This is <em>totally <strong>unquestionably</strong></em> <strong>strongly</strong> made of text!"
+      assert_includes trim_html_ids(para("@p2").display.to_html), "And <em>totally <strong>unquestionably</strong></em> a great idea!"
+
+      # Check .text updating
+      assert_equal "This is totally unquestionably strongly made of text!", para("@p1").text
+      assert_equal "And totally unquestionably a great idea!", para("@p2").text
+
+      # Check dom_html updating
+      h = trim_html_ids(dom_html) # Query once
+      assert !h.include?("emphatically"), "the word 'emphatically' should have been removed by the update"
+      assert_includes h, "This is <em>totally <strong>unquestionably</strong></em> <strong>strongly</strong> made of text!"
+      assert_includes h, "And <em>totally <strong>unquestionably</strong></em> a great idea!"
+    SSPEC
+  end
+
+  def test_span
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is ", em("emphatically"), " made of ", span("text!")
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_equal "This is emphatically made of text!", para.text
+      assert_includes trim_html_ids(para.display.to_html), "This is <em>emphatically</em> made of <span>text!</span>"
+    SSPEC
+  end
+
+  def test_basic_link
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is ", link(em("emphatically"), " made of ", span("text!"), click: "http://foo.com")
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_equal "This is emphatically made of text!", para.text
+      assert_includes trim_html_ids(para.display.to_html),
+        \%{This is <a href="http://foo.com"><em>emphatically</em> made of <span>text!</span></a>}
+    SSPEC
+  end
+
+  def test_default_para_size
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        style Shoes::Para, size: 18
+        para "This is made of text!"
+      end
+      ----------- test code
+      assert_includes para.display.to_html, "font-size:18px"
+    SSPEC
+  end
+
+  def test_default_para_size_override
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        style Shoes::Para, size: 14
+        para "This is made of text!", size: 19
+      end
+      ----------- test code
+      assert_includes para.display.to_html, "font-size:19px"
+    SSPEC
+  end
+
+  def test_default_em_size
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        style Shoes::Em, size: 21
+        para "This is made of ", em("text!")
+      end
+      ----------- test code
+      assert_includes para.display.to_html, "font-size:21px"
+    SSPEC
+  end
+
+  def test_default_em_size_override
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        style Shoes::Em, size: 14
+        para "This is made of ", em("text!", size: 21)
+      end
+      ----------- test code
+      assert_includes para.display.to_html, "font-size:21px"
+    SSPEC
+  end
+
+  def test_size_on_para_and_text_drawable
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is ", em("emphatically", size: 16), " made of text!", size: 14
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_includes trim_html_ids(para.display.to_html),
+        \%{This is <em style="font-size:16px">emphatically</em> made of text!}
+      assert_includes trim_html_ids(para.display.to_html), "font-size:14px"
+    SSPEC
+  end
+
+  def test_bug_with_confusing_ins_and_inscription
+    run_scarpe_sspec_code(<<~SSPEC)
+      ---
+      ----------- app code
+      Shoes.app do
+        para "Various ", del("text"), " in ", sub("various"), " ", sup("styles"),
+          " can be ", ins("hard to read"), "...\n"
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      h = trim_html_ids(dom_html)
+
+      assert_includes trim_html_ids(para.display.to_html),
+        \%{Various <del>text</del> in <sub>various</sub> <sup>styles</sup> can be <span style=\"text-decoration-line:underline\"><span>hard to read</span></span>...}
+    SSPEC
+  end
+
+  def test_tiranti_big_text_para_tags
+    run_scarpe_sspec_code(<<~SSPEC, html_renderer: "tiranti")
+      ---
+      ----------- app code
+      Shoes.app do
+        para "This is made of text!", size: 26
+      end
+      ----------- test code
+      #{require_helper}
+      self.class.include TextDrawableHelper
+
+      assert_equal \%{<h3 id=\"3\" style=\"font-size:26px\">This is made of text!</h3>},
+        para.display.to_html
+    SSPEC
+  end
+
+end
diff --git a/test/wv/html_fixtures/link.html b/test/wv/html_fixtures/link.html
index e8c3e5f8a..1970aa125 100644
--- a/test/wv/html_fixtures/link.html
+++ b/test/wv/html_fixtures/link.html
@@ -1,6 +1,6 @@
 <div id="2" style="display:flex;flex-direction:row;flex-wrap:wrap;align-content:flex-start;justify-content:flex-start;align-items:flex-start;width:100%;height:100%">
   <div style="height:100%;width:100%;position:relative">
-    <p id="4" style="font-size:12px">'Scarpe' means shoes in Italian. 'Scarpe' also means Shoes in...<a id="3" onclick="scarpeHandler('3-click')">(show more)</a></p>
+    <p id="4" style="font-size:12px">'Scarpe' means shoes in Italian. 'Scarpe' also means Shoes in...<a class="id_3" onclick="scarpeHandler('3-click')">(show more)</a></p>
     <div id="root-fonts"></div>
     <div id="root-alerts"> </div>
   </div>
diff --git a/test/wv/html_fixtures/para_text_widgets.html b/test/wv/html_fixtures/para_text_widgets.html
index ef5d33853..49fb4e43a 100644
--- a/test/wv/html_fixtures/para_text_widgets.html
+++ b/test/wv/html_fixtures/para_text_widgets.html
@@ -1,7 +1,7 @@
 <div id="2" style="display:flex;flex-direction:row;flex-wrap:wrap;align-content:flex-start;justify-content:flex-start;align-items:flex-start;width:100%;height:100%">
   <div style="height:100%;width:100%;position:relative">
     <p id="3" style="font-size:12px">This is simple.</p>
-    <p id="7" style="font-size:12px">This has <em>emphasis</em> and great <strong>strength</strong> and <code>coolness</code>.</p>
+    <p id="7" style="font-size:12px">This has <em class="id_4">emphasis</em> and great <strong class="id_5">strength</strong> and <code class="id_6">coolness</code>.</p>
     <div id="root-fonts"></div>
     <div id="root-alerts"> </div>
   </div>
diff --git a/test/wv/html_fixtures/shoes_splorer.html b/test/wv/html_fixtures/shoes_splorer.html
index 45751777c..929240fad 100644
--- a/test/wv/html_fixtures/shoes_splorer.html
+++ b/test/wv/html_fixtures/shoes_splorer.html
@@ -1,14 +1,14 @@
 <div id="2" style="display:flex;flex-direction:row;flex-wrap:wrap;align-content:flex-start;justify-content:flex-start;align-items:flex-start;width:100%;height:100%">
   <div style="height:100%;width:100%;position:relative">
     <p id="3" style="font-size:12px">What Shoes methods are available?</p>
-    <p id="5" style="color:#008000;font-size:12px"><strong> animate[Woot!] </strong></p>
-    <p id="14" style="color:#FFA500;font-size:12px">o<code>arrow</code> o<code>arc</code> o<code>line</code> o<code>oval</code> o<code>rect</code> o<code>star</code> o<code>shape</code> x<code>mask</code> </p>
-    <p id="16" style="color:#008000;font-size:12px"><strong> element[Woot!] </strong></p>
-    <p id="30" style="color:#FFA500;font-size:12px">x<code>mouse</code> o<code>motion</code> x<code>resize</code> o<code>hover</code> o<code>leave</code> o<code>keypress</code> x<code>keyrelease</code> x<code>append</code> x<code>visit</code> x<code>scroll_top</code> x<code>clipboard</code> o<code>download</code> x<code>gutter</code> </p>
-    <p id="34" style="color:#FFA500;font-size:12px">o<code>image</code> o<code>video</code> x<code>sound</code> </p>
-    <p id="37" style="color:#FF0000;font-size:12px"><em> xxx</em>setup<em>xxx </em></p>
-    <p id="48" style="color:#FFA500;font-size:12px">o<code>style</code> o<code>fill</code> o<code>stroke</code> x<code>cap</code> o<code>rotate</code> o<code>strokewidth</code> x<code>transform</code> x<code>translate</code> o<code>nostroke</code> o<code>nofill</code> </p>
-    <p id="67" style="color:#FFA500;font-size:12px">o<code>banner</code> o<code>title</code> o<code>subtitle</code> o<code>tagline</code> o<code>caption</code> o<code>para</code> o<code>inscription</code> o<code>code</code> x<code>del</code> o<code>em</code> o<code>ins</code> x<code>sub</code> x<code>sup</code> o<code>strong</code> x<code>fg</code> x<code>bg</code> o<code>link</code> o<code>span</code> </p>
+    <p id="5" style="color:#008000;font-size:12px"><strong class="id_4"> animate[Woot!] </strong></p>
+    <p id="14" style="color:#FFA500;font-size:12px">o<code class="id_6">arrow</code> o<code class="id_7">arc</code> o<code class="id_8">line</code> o<code class="id_9">oval</code> o<code class="id_10">rect</code> o<code class="id_11">star</code> o<code class="id_12">shape</code> x<code class="id_13">mask</code> </p>
+    <p id="16" style="color:#008000;font-size:12px"><strong class="id_15"> element[Woot!] </strong></p>
+    <p id="30" style="color:#FFA500;font-size:12px">x<code class="id_17">mouse</code> o<code class="id_18">motion</code> x<code class="id_19">resize</code> o<code class="id_20">hover</code> o<code class="id_21">leave</code> o<code class="id_22">keypress</code> x<code class="id_23">keyrelease</code> x<code class="id_24">append</code> x<code class="id_25">visit</code> x<code class="id_26">scroll_top</code> x<code class="id_27">clipboard</code> o<code class="id_28">download</code> x<code class="id_29">gutter</code> </p>
+    <p id="34" style="color:#FFA500;font-size:12px">o<code class="id_31">image</code> o<code class="id_32">video</code> x<code class="id_33">sound</code> </p>
+    <p id="37" style="color:#FF0000;font-size:12px"><em class="id_35"> xxx</em>setup<em class="id_36">xxx </em></p>
+    <p id="48" style="color:#FFA500;font-size:12px">o<code class="id_38">style</code> o<code class="id_39">fill</code> o<code class="id_40">stroke</code> x<code class="id_41">cap</code> o<code class="id_42">rotate</code> o<code class="id_43">strokewidth</code> x<code class="id_44">transform</code> x<code class="id_45">translate</code> o<code class="id_46">nostroke</code> o<code class="id_47">nofill</code> </p>
+    <p id="67" style="color:#FFA500;font-size:12px">o<code class="id_49">banner</code> o<code class="id_50">title</code> o<code class="id_51">subtitle</code> o<code class="id_52">tagline</code> o<code class="id_53">caption</code> o<code class="id_54">para</code> o<code class="id_55">inscription</code> o<code class="id_56">code</code> o<code class="id_57">del</code> o<code class="id_58">em</code> o<code class="id_59">ins</code> o<code class="id_60">sub</code> o<code class="id_61">sup</code> o<code class="id_62">strong</code> x<code class="id_63">fg</code> x<code class="id_64">bg</code> o<code class="id_65">link</code> o<code class="id_66">span</code> </p>
     <div id="root-fonts"></div>
     <div id="root-alerts"> </div>
   </div>
diff --git a/test/wv/html_fixtures/simple_slides.html b/test/wv/html_fixtures/simple_slides.html
index a292f7857..d2987bfcf 100644
--- a/test/wv/html_fixtures/simple_slides.html
+++ b/test/wv/html_fixtures/simple_slides.html
@@ -4,7 +4,7 @@
       <div style="height:100%;width:100%;position:relative"></div>
     </div>
     <button id="4" onclick="scarpeHandler('4-click')" onmouseover="scarpeHandler('4-hover')">Previous Slide</button><button id="5" onclick="scarpeHandler('5-click')" onmouseover="scarpeHandler('5-hover')">Next Slide</button>
-    <p id="7" style="margin-bottom:10px;font-size:12px"><strong>Slide 1: Welcome to Shoes!</strong></p>
+    <p id="7" style="margin-bottom:10px;font-size:12px"><strong class="id_6">Slide 1: Welcome to Shoes!</strong></p>
     <p id="8" style="font-size:12px">Ah, yeah, Shoes!<br>
             Who said nobody knows Shoes? Well, let me introduce you to a feline expert in the art of Ruby Desktop GUI Libs.<br>
       <br>
diff --git a/test/wv/html_fixtures/span.html b/test/wv/html_fixtures/span.html
index 31c96da39..01a17bc42 100644
--- a/test/wv/html_fixtures/span.html
+++ b/test/wv/html_fixtures/span.html
@@ -1,11 +1,13 @@
 <div id="2" style="display:flex;flex-direction:row;flex-wrap:wrap;align-content:flex-start;justify-content:flex-start;align-items:flex-start;width:100%;height:100%">
   <div style="height:100%;width:100%;position:relative">
     <div id="3" style="display:flex;flex-direction:column;align-content:flex-start;justify-content:flex-start;align-items:flex-start;margin:10px">
-      <div style="height:100%;width:100%;position:relative"><span id="4" style="color:blue;font-size:span">TEXT EDITOR</span>
-        <p id="5" style="color:#FF0000;font-size:12px"><span id="4" style="color:blue;font-size:span">TEXT EDITOR</span> * USE ALT-Q TO QUIT</p>
+      <div style="height:100%;width:100%;position:relative">
+        <p id="5" style="color:#FF0000;font-size:12px"><span class="id_4" style="color:#0000FF;background-color:#008000">TEXT EDITOR</span> * USE ALT-Q TO QUIT</p>
       </div>
     </div>
-    <span id="6" style="font-size:span">text</span>
+    <p id="10" style="font-size:12px">Various <del class="id_6">text</del> in <sub class="id_7">various</sub> <sup class="id_8">styles</sup> can be <span class="id_9" style="text-decoration-line:underline"><span class="id_9">hard to read</span></span>...<br>
+    </p>
+    <p id="15" style="font-size:12px">A <span class="id_11" style="text-decoration-color:#0000FF;text-decoration-line:underline"><span class="id_11">wide</span></span> <span class="id_12" style="text-decoration-color:#008000;text-decoration-line:underline;text-decoration-style:wavy"><span class="id_12">variety</span></span> <span class="id_13" style="text-decoration-line:underline;text-decoration-style:double"><span class="id_13">of</span></span> <span class="id_14" style="text-decoration-color:#006400;text-decoration-line:underline;text-underline-offset:0.3rem"><span class="id_14">underlines</span></span></p>
     <div id="root-fonts"></div>
     <div id="root-alerts"> </div>
   </div>
diff --git a/test/wv/html_fixtures/text_sizes.html b/test/wv/html_fixtures/text_sizes.html
index 1e6f1bc1d..ba38de8f1 100644
--- a/test/wv/html_fixtures/text_sizes.html
+++ b/test/wv/html_fixtures/text_sizes.html
@@ -7,7 +7,6 @@
     <p id="7" style="font-size:14px">Caption</p>
     <p id="8" style="font-size:12px">Para</p>
     <p id="9" style="font-size:10px">Inscription</p>
-    <p id="10" style="font-size:10px">Inscription via ins</p>
     <div id="root-fonts"></div>
     <div id="root-alerts"> </div>
   </div>