diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Revision history for typst-hs
 
+## 0.12
+
+  * Support 'in' operator on modules (#106).
+
+  * Return an error instead of crashing on integer division by zero (#103,
+    Vladimir Babin).
+
+  * Add support for the stroke type (#76, #107, Samuel Huang)
+    [API change] Adds `VStroke` to `Val`, `TStroke` to `ValType`,
+    `Stroke`, `emptyStroke`.
+
 ## 0.11.0.1
 
   * Fix `calc.pow` so it accepts negative exponents (#102).
diff --git a/src/Typst/Constructors.hs b/src/Typst/Constructors.hs
--- a/src/Typst/Constructors.hs
+++ b/src/Typst/Constructors.hs
@@ -25,7 +25,7 @@
 import Typst.Regex (makeRE)
 import Data.List (genericTake)
 import Control.Monad.Reader (asks, lift)
-import Typst.Module.Standard (getPath)
+import Typst.Module.Standard (getPath, strokeConstructor)
 import Control.Monad (mplus)
 import Data.Char (ord, chr, isDigit, isAsciiLower, isAsciiUpper)
 
@@ -81,6 +81,7 @@
              pure $ VString $ T.pack [chr val] )
       ]
     TLabel -> Just $ makeFunction $ VLabel <$> nthArg 1
+    TStroke -> Just strokeConstructor
     TSymbol -> Just $ makeFunction $ do
         (t :: Text) <- nthArg 1
         vs <- drop 1 <$> allArgs
diff --git a/src/Typst/Evaluate.hs b/src/Typst/Evaluate.hs
--- a/src/Typst/Evaluate.hs
+++ b/src/Typst/Evaluate.hs
@@ -734,6 +734,10 @@
           case v1 of
             VString t -> pure $ VBoolean $ isJust $ OM.lookup (Identifier t) m
             _ -> pure $ VBoolean False
+        VModule _ m ->
+          case v1 of
+            VString t -> pure $ VBoolean $ isJust $ M.lookup (Identifier t) m
+            _ -> pure $ VBoolean False
         _ -> fail $ "Can't apply 'in' to " <> show v2 <> show (e1,e2)
 
     Negated e -> do
diff --git a/src/Typst/Methods.hs b/src/Typst/Methods.hs
--- a/src/Typst/Methods.hs
+++ b/src/Typst/Methods.hs
@@ -154,6 +154,14 @@
             CMYK c m y k -> CMYK (1 - c) (1 - m) (1 - y) k
             Luma x -> Luma (1 - x)
         _ -> noMethod "Color" fld
+    VStroke s ->
+      case fld of
+        "paint" -> pure $ maybe VAuto VColor (paint s)
+        "thickness" -> pure $ maybe VAuto VLength (thickness s)
+        "cap" -> pure $ maybe VAuto VString (cap s)
+        "join" -> pure $ maybe VAuto VString (join s)
+        "miter-limit" -> pure $ maybe VAuto VFloat (miterLimit s)
+        _ -> noMethod "Stroke" fld
     VString t -> do
       let toPos n =
             if n < 0
diff --git a/src/Typst/Module/Calc.hs b/src/Typst/Module/Calc.hs
--- a/src/Typst/Module/Calc.hs
+++ b/src/Typst/Module/Calc.hs
@@ -157,16 +157,21 @@
         makeFunction $ do
           (a :: Integer) <- nthArg 1
           (b :: Integer) <- nthArg 2
-          pure $ VInteger $ a `quot` b
+          if b == 0
+            then fail "division by zero"
+            else pure $ VInteger $ a `quot` b
       ),
       ( "rem",
         makeFunction $ do
           (a :: Integer, f :: Double) <- properFraction <$> nthArg 1
           (b :: Integer) <- nthArg 2
-          pure $
-            if f == 0
-              then VInteger $ rem a b
-              else VFloat $ fromIntegral (rem a b) + f
+          if b == 0
+            then fail "division by zero"
+            else
+              pure $
+                if f == 0
+                  then VInteger $ rem a b
+                  else VFloat $ fromIntegral (rem a b) + f
       ),
       ( "round",
         makeFunction $ do
diff --git a/src/Typst/Module/Standard.hs b/src/Typst/Module/Standard.hs
--- a/src/Typst/Module/Standard.hs
+++ b/src/Typst/Module/Standard.hs
@@ -10,7 +10,8 @@
     loadFileText,
     getPath,
     applyPureFunction,
-    elementDefaults
+    elementDefaults,
+    strokeConstructor
   )
 where
 
@@ -400,6 +401,9 @@
   , ("length", VType TLength)
   , ("alignment", VType TAlignment)
   , ("color", VType TColor)
+  -- The stroke type is bound to VType, like other types; stroke(...)
+  -- works because VType values are callable via getConstructor.
+  , ("stroke", VType TStroke)
   , ("symbol", VType TSymbol)
   , ("str", VType TString)
   , ("label", VType TLabel)
@@ -547,6 +551,53 @@
     )
   ]
 
+-- | The stroke() constructor, also used by getConstructor for TStroke.
+strokeConstructor :: Val
+strokeConstructor =
+  makeFunction $ do
+    base <-
+      nthArg 1 >>= \case
+        VNone -> pure emptyStroke
+        VStroke s -> pure s
+        VColor c -> pure emptyStroke { paint = Just c }
+        VLength l -> pure emptyStroke { thickness = Just l }
+        VDict m -> strokeFromDict m
+        _ -> fail "expected stroke, color, length, or dictionary"
+    -- getNamed (not namedArg) so an explicit `none` is distinguishable
+    -- from an absent argument; like `auto`, it resets the field.
+    -- (typst errors on `none` here, and rejects combining a base with
+    -- named arguments.)
+    --
+    -- This can't be reduced to `mergeStrokes base <$> strokeFromDict
+    -- named`: mergeStrokes falls back to the base field when a named
+    -- argument is explicitly none or auto, so a reset would silently
+    -- become an inherit.
+    mbPaint <- getNamed "paint"
+    mbThickness <- getNamed "thickness"
+    mbCap <- getNamed "cap"
+    mbJoin <- getNamed "join"
+    mbMiterLimit <- getNamed "miter-limit"
+    -- A named argument overrides the base field; auto or none resets it.
+    let override get f mb = case mb of
+          Nothing -> pure (get base)
+          Just VAuto -> pure Nothing
+          Just VNone -> pure Nothing
+          Just v -> Just <$> f v
+    paint <- override paint asColor mbPaint
+    thickness <- override thickness asLength mbThickness
+    cap <- override cap asLineCap mbCap
+    join <- override join asLineJoin mbJoin
+    miterLimit <- override miterLimit asMiterLimit mbMiterLimit
+    pure $
+      VStroke $
+        emptyStroke
+          { paint = paint,
+            thickness = thickness,
+            cap = cap,
+            join = join,
+            miterLimit = miterLimit
+          }
+
 loremWords :: [Text]
 loremWords =
   cycle $
@@ -739,3 +790,52 @@
   case v of
     VBytes bs -> pure $ BL.fromStrict bs
     _ -> lift $ resolvePathVal v >>= loadResolvedLazyBytes
+
+-- | Build a 'Stroke' from a dictionary such as
+-- @(paint: red, thickness: 2pt)@.
+strokeFromDict :: MonadFail m => OM.OMap Identifier Val -> m Stroke
+strokeFromDict m = do
+  let field k f = case OM.lookup k m of
+        Nothing -> pure Nothing
+        Just VAuto -> pure Nothing
+        Just VNone -> pure Nothing
+        Just v -> Just <$> f v
+  paint <- field "paint" asColor
+  thickness <- field "thickness" asLength
+  cap <- field "cap" asLineCap
+  join <- field "join" asLineJoin
+  miterLimit <- field "miter-limit" asMiterLimit
+  -- Unknown keys (e.g. `dash` until it is supported) are ignored;
+  -- typst rejects them.
+  pure $
+    emptyStroke
+      { paint = paint,
+        thickness = thickness,
+        cap = cap,
+        join = join,
+        miterLimit = miterLimit
+      }
+
+asColor :: MonadFail m => Val -> m Color
+asColor (VColor c) = pure c
+asColor _ = fail "paint must be a color"
+
+asLength :: MonadFail m => Val -> m Length
+asLength (VLength l) = pure l
+asLength _ = fail "thickness must be a length"
+
+-- typst only defines these for specific strings, but we accept any
+-- string (a superset of what typst accepts).
+asLineCap :: MonadFail m => Val -> m Text
+asLineCap (VString s) = pure s
+asLineCap _ = fail "cap must be a string"
+
+asLineJoin :: MonadFail m => Val -> m Text
+asLineJoin (VString s) = pure s
+asLineJoin _ = fail "join must be a string"
+
+asMiterLimit :: MonadFail m => Val -> m Double
+asMiterLimit (VFloat x) = pure x
+asMiterLimit (VInteger x) = pure (fromIntegral x)
+asMiterLimit (VRatio x) = pure (fromRational x)
+asMiterLimit _ = fail "miter-limit must be a number"
diff --git a/src/Typst/Types.hs b/src/Typst/Types.hs
--- a/src/Typst/Types.hs
+++ b/src/Typst/Types.hs
@@ -39,6 +39,8 @@
     Horiz (..),
     Vert (..),
     Color (..),
+    Stroke (..),
+    emptyStroke,
     Direction (..),
     Identifier (..), -- reexported
     lookupIdentifier,
@@ -60,7 +62,7 @@
 import Data.Functor.Classes (Ord1 (liftCompare))
 import qualified Data.Map as M
 import qualified Data.Map.Ordered as OM
-import Data.Maybe (fromMaybe, isJust, catMaybes)
+import Data.Maybe (fromMaybe, isJust, isNothing, catMaybes)
 import Data.Scientific (floatingOrInteger)
 import Data.Sequence (Seq)
 import qualified Data.Sequence as Seq
@@ -115,6 +117,8 @@
   -- only @rgb@, @cmyk@, and @luma@ are available. 
   -- See issue [#35](https://github.com/jgm/typst-hs/issues/35#issuecomment-1926182040).
   | VColor !Color
+  -- | A @stroke@ value, representing stroke properties for shapes and lines.
+  | VStroke !Stroke
   -- | A @symbol@ value, representing a Unicode symbol.
   | VSymbol !Symbol
   -- | A UTF-8 encoded text @string@.
@@ -191,6 +195,7 @@
   | TAngle
   | TFraction
   | TColor
+  | TStroke
   | TSymbol
   | TString
   | TRegex
@@ -230,6 +235,7 @@
     VAngle {} -> TAngle
     VFraction {} -> TFraction
     VColor {} -> TColor
+    VStroke {} -> TStroke
     VSymbol {} -> TSymbol
     VString {} -> TString
     VRegex {} -> TRegex
@@ -407,6 +413,8 @@
   comp (VAngle x1) (VAngle x2) = Just $ compare x1 x2
   comp (VFraction x1) (VFraction x2) = Just $ compare x1 x2
   comp (VColor c1) (VColor c2) = Just $ compare c1 c2
+  comp (VStroke s1) (VStroke s2) =
+    if s1 == s2 then Just EQ else Nothing
   comp (VSymbol (Symbol s1 _ _)) (VSymbol (Symbol s2 _ _)) = Just $ compare s1 s2
   comp (VString s1) (VString s2) = Just $ compare s1 s2
   comp (VPath p1) (VPath p2) = Just $ compare p1 p2
@@ -473,11 +481,26 @@
   maybePlus (VFraction f1) (VFraction f2) = pure $ VFraction (f1 + f2)
   maybePlus (VArray v1) (VArray v2) = pure $ VArray (v1 <> v2)
   maybePlus (VDict m1) (VDict m2) = pure $ VDict (m1 OM.<>| m2)
+  -- Stroke '1pt + red'
   maybePlus (VColor c) (VLength l) =
-    -- Stroke '1pt + red'
-    pure $ VDict $ OM.fromList [("thickness", VLength l), ("color", VColor c)]
+    pure $ VStroke $ emptyStroke { paint = Just c, thickness = Just l }
   maybePlus (VLength l) (VColor c) = maybePlus (VColor c) (VLength l)
+  -- typst-hs extension: adding to a stroke refines its fields; adding
+  -- a stroke merges per field, with the right operand taking precedence.
+  maybePlus (VStroke s) (VColor c) = pure $ VStroke s { paint = Just c }
+  maybePlus (VStroke s) (VLength l) = pure $ VStroke s { thickness = Just l }
+  maybePlus (VStroke s1) (VStroke s2) = pure $ VStroke $ mergeStrokes s1 s2
+  maybePlus (VColor c) (VStroke s) =
+    pure $ VStroke $ mergeStrokes (emptyStroke { paint = Just c }) s
+  maybePlus (VLength l) (VStroke s) =
+    pure $ VStroke $ mergeStrokes (emptyStroke { thickness = Just l }) s
   maybePlus v1 v2 = fail $ "could not add " <> show v1 <> " and " <> show v2
+  -- Typst has no color - length or stroke - length; block the default
+  -- negate-and-add, which would otherwise produce a stroke with
+  -- negative thickness.
+  maybeMinus (VColor _) (VLength _) = Nothing
+  maybeMinus (VStroke _) (VLength _) = Nothing
+  maybeMinus v1 v2 = maybeNegate v2 >>= maybePlus v1
 
 class Multipliable a where
   maybeTimes :: a -> a -> Maybe a
@@ -523,9 +546,11 @@
   maybeTimes v1 v2 = fail $ "could not multiply " <> show v1 <> " and " <> show v2
 
   maybeDividedBy (VInteger i1) (VInteger i2) =
-    if i1 `mod` i2 == 0
-      then pure $ VInteger (i1 `div` i2)
-      else pure $ VFloat (fromIntegral i1 / fromIntegral i2)
+    if i2 == 0
+      then fail "division by zero"
+      else if i1 `mod` i2 == 0
+        then pure $ VInteger (i1 `div` i2)
+        else pure $ VFloat (fromIntegral i1 / fromIntegral i2)
   maybeDividedBy (VFloat x1) (VFloat x2) = maybeTimes (VFloat x1) (VFloat (1 / x2))
   maybeDividedBy (VInteger i1) (VFloat f2) = pure $ VFloat (fromIntegral i1 / f2)
   maybeDividedBy (VFloat f1) (VInteger i2) = pure $ VFloat (f1 / fromIntegral i2)
@@ -828,6 +853,31 @@
   | Luma Rational
   deriving (Show, Eq, Ord, Typeable)
 
+data Stroke = Stroke
+  { paint :: !(Maybe Color), -- Nothing = auto (default: black)
+    thickness :: !(Maybe Length), -- Nothing = auto (default: 1pt)
+    cap :: !(Maybe Text), -- Nothing = auto (default: "butt")
+    join :: !(Maybe Text), -- Nothing = auto (default: "miter")
+    miterLimit :: !(Maybe Double) -- Nothing = auto (default: 4.0)
+  }
+  deriving (Show, Eq, Typeable)
+
+-- | A stroke with every field unset (auto).
+emptyStroke :: Stroke
+emptyStroke = Stroke Nothing Nothing Nothing Nothing Nothing
+
+-- | Merge two strokes, with fields set on the second taking precedence
+-- (the same per-field semantics as typst's Fold for strokes).
+mergeStrokes :: Stroke -> Stroke -> Stroke
+mergeStrokes a b =
+  Stroke
+    { paint = paint b `mplus` paint a,
+      thickness = thickness b `mplus` thickness a,
+      cap = cap b `mplus` cap a,
+      join = join b `mplus` join a,
+      miterLimit = miterLimit b `mplus` miterLimit a
+    }
+
 data Direction 
   = Ltr -- ^ Left to right
   | Rtl -- ^ Right to left
@@ -835,6 +885,11 @@
   | Btt -- ^ Bottom to top
   deriving (Show, Eq, Ord, Typeable)
 
+-- | Render a set stroke field for the parenthesized stroke repr.
+strokeField :: Text -> Maybe P.Doc -> [P.Doc]
+strokeField _ Nothing = []
+strokeField k (Just v) = [text k <> ": " <> v]
+
 prettyVal :: Val -> P.Doc
 prettyVal expr =
   case expr of
@@ -885,6 +940,28 @@
     VFunction _ _ _ -> mempty
     VLabel t -> text $ "<" <> t <> ">"
     VCounter _ -> mempty
+    VStroke s ->
+      -- Matches typst's Repr for Stroke: the simple stroke forms are
+      -- used when only paint and thickness are set, otherwise a
+      -- parenthesized list of the set fields, in typst's order.
+      if isNothing (cap s) && isNothing (join s) && isNothing (miterLimit s)
+        then case (paint s, thickness s) of
+          (Just p, Just t) ->
+            prettyVal (VLength t) <> " + " <> prettyVal (VColor p)
+          (Just p, Nothing) -> prettyVal (VColor p)
+          (Nothing, Just t) -> prettyVal (VLength t)
+          -- typst hardcodes "1pt + black" for the fully-auto stroke
+          -- (stroke.rs), even though that repr denotes explicit fields.
+          (Nothing, Nothing) -> "1pt + black"
+        else
+          -- hcat, so the repr is a single line as in typst
+          P.parens . P.hcat . P.punctuate ", " . concat $
+            ([ strokeField "paint" (prettyVal . VColor <$> paint s),
+               strokeField "thickness" (prettyVal . VLength <$> thickness s),
+               strokeField "cap" (prettyVal . VString <$> cap s),
+               strokeField "join" (prettyVal . VString <$> join s),
+               strokeField "miter-limit" (prettyVal . VFloat <$> miterLimit s)
+             ] :: [[P.Doc]])
     VColor (RGB r g b o) ->
       "rgb("
         <> text (toPercent r)
diff --git a/src/Typst/Util.hs b/src/Typst/Util.hs
--- a/src/Typst/Util.hs
+++ b/src/Typst/Util.hs
@@ -13,6 +13,7 @@
     argsToFields,
     nthArg,
     namedArg,
+    getNamed,
     allArgs
   )
 where
diff --git a/test/typ/compiler/repr-02.out b/test/typ/compiler/repr-02.out
--- a/test/typ/compiler/repr-02.out
+++ b/test/typ/compiler/repr-02.out
@@ -95,8 +95,7 @@
                       size: 0.8em), 
                  text(body: [ ], size: 0.8em), 
                  linebreak(), 
-                 text(body: [(thickness: 2.0pt,
- color: rgb(96%,63%,1%,100%))], 
+                 text(body: [2.0pt + rgb(96%,63%,1%,100%)], 
                       size: 0.8em), 
                  parbreak(), 
                  text(body: [
diff --git a/test/typ/compiler/show-selector-00.out b/test/typ/compiler/show-selector-00.out
--- a/test/typ/compiler/show-selector-00.out
+++ b/test/typ/compiler/show-selector-00.out
@@ -150,8 +150,7 @@
                        fill: luma(23000%), 
                        inset: 11.0pt, 
                        outset: -3.0pt, 
-                       stroke: (left: (thickness: 1.5pt,
-                                       color: luma(18000%)))), 
+                       stroke: (left: 1.5pt + luma(18000%))), 
                  parbreak(), 
                  text(body: [You can use the ]), 
                  block(body: raw(block: true, 
@@ -160,8 +159,7 @@
                        fill: luma(23000%), 
                        inset: 11.0pt, 
                        outset: -3.0pt, 
-                       stroke: (left: (thickness: 1.5pt,
-                                       color: luma(18000%)))), 
+                       stroke: (left: 1.5pt + luma(18000%))), 
                  text(body: [ pointer or
 the ]), 
                  block(body: raw(block: true, 
@@ -170,7 +168,6 @@
                        fill: luma(23000%), 
                        inset: 11.0pt, 
                        outset: -3.0pt, 
-                       stroke: (left: (thickness: 1.5pt,
-                                       color: luma(18000%)))), 
+                       stroke: (left: 1.5pt + luma(18000%))), 
                  text(body: [ reference.]), 
                  parbreak() })
diff --git a/test/typ/issue-106.out b/test/typ/issue-106.out
new file mode 100644
--- /dev/null
+++ b/test/typ/issue-106.out
@@ -0,0 +1,83 @@
+--- parse tree ---
+[ Comment
+, SoftBreak
+, Code
+    "typ/issue-106.typ"
+    ( line 2 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (InCollection
+              (Literal (String "divider")) (Ident (Identifier "std")))
+       , NormalArg (Literal (Boolean True))
+       ])
+, SoftBreak
+, Code
+    "typ/issue-106.typ"
+    ( line 3 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (InCollection
+              (Literal (String "nonexistent")) (Ident (Identifier "std")))
+       , NormalArg (Literal (Boolean False))
+       ])
+, SoftBreak
+, Code
+    "typ/issue-106.typ"
+    ( line 4 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Not
+              (InCollection
+                 (Literal (String "divider")) (Ident (Identifier "std"))))
+       , NormalArg (Literal (Boolean False))
+       ])
+, SoftBreak
+, Code
+    "typ/issue-106.typ"
+    ( line 5 , column 2 )
+    (Let
+       (BasicBind (Just (Identifier "divider")))
+       (If
+          [ ( InCollection
+                (Literal (String "divider")) (Ident (Identifier "std"))
+            , Block (CodeBlock [ Ident (Identifier "divider") ])
+            )
+          , ( Literal (Boolean True) , Block (CodeBlock [ Literal None ]) )
+          ]))
+, SoftBreak
+, Code
+    "typ/issue-106.typ"
+    ( line 6 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "str"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "type"))
+                     [ NormalArg (Ident (Identifier "divider")) ])
+              ])
+       , NormalArg (Literal (String "function"))
+       ])
+, ParBreak
+]
+--- evaluated ---
+document(body: { text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak() })
diff --git a/test/typ/issue-106.typ b/test/typ/issue-106.typ
new file mode 100644
--- /dev/null
+++ b/test/typ/issue-106.typ
@@ -0,0 +1,6 @@
+// Test 'in' operator on a module (issue #106).
+#test("divider" in std, true)
+#test("nonexistent" in std, false)
+#test("divider" not in std, false)
+#let divider = if "divider" in std { divider } else { none }
+#test(str(type(divider)), "function")
diff --git a/test/typ/layout/clip-01.out b/test/typ/layout/clip-01.out
--- a/test/typ/layout/clip-01.out
+++ b/test/typ/layout/clip-01.out
@@ -94,8 +94,7 @@
                                parbreak() }, 
                        clip: false, 
                        height: 2.0em, 
-                       stroke: (thickness: 1.0pt,
-                                color: rgb(0%,0%,0%,100%)), 
+                       stroke: 1.0pt + rgb(0%,0%,0%,100%), 
                        width: 5.0em), 
                  parbreak(), 
                  v(amount: 2.0em), 
@@ -106,7 +105,6 @@
                                parbreak() }, 
                        clip: true, 
                        height: 2.0em, 
-                       stroke: (thickness: 1.0pt,
-                                color: rgb(0%,0%,0%,100%)), 
+                       stroke: 1.0pt + rgb(0%,0%,0%,100%), 
                        width: 5.0em), 
                  parbreak() })
diff --git a/test/typ/layout/clip-02.out b/test/typ/layout/clip-02.out
--- a/test/typ/layout/clip-02.out
+++ b/test/typ/layout/clip-02.out
@@ -50,13 +50,11 @@
 Emoji: ]), 
                  box(body: text(body: [🐪, 🌋, 🏞]), 
                      height: 0.5em, 
-                     stroke: (thickness: 1.0pt,
-                              color: rgb(0%,0%,0%,100%))), 
+                     stroke: 1.0pt + rgb(0%,0%,0%,100%)), 
                  parbreak(), 
                  text(body: [Emoji: ]), 
                  box(body: text(body: [🐪, 🌋, 🏞]), 
                      clip: true, 
                      height: 0.5em, 
-                     stroke: (thickness: 1.0pt,
-                              color: rgb(0%,0%,0%,100%))), 
+                     stroke: 1.0pt + rgb(0%,0%,0%,100%)), 
                  parbreak() })
diff --git a/test/typ/layout/clip-03.out b/test/typ/layout/clip-03.out
--- a/test/typ/layout/clip-03.out
+++ b/test/typ/layout/clip-03.out
@@ -72,6 +72,5 @@
                                parbreak() }, 
                        clip: true, 
                        height: 4.0em, 
-                       stroke: (thickness: 1.0pt,
-                                color: rgb(0%,0%,0%,100%))), 
+                       stroke: 1.0pt + rgb(0%,0%,0%,100%)), 
                  parbreak() })
diff --git a/test/typ/layout/table-00.out b/test/typ/layout/table-00.out
--- a/test/typ/layout/table-00.out
+++ b/test/typ/layout/table-00.out
@@ -94,6 +94,5 @@
                                  1.0fr, 
                                  1.0fr), 
                        fill: , 
-                       stroke: (thickness: 2.0pt,
-                                color: rgb(1%,1%,1%,100%))), 
+                       stroke: 2.0pt + rgb(1%,1%,1%,100%)), 
                  parbreak() })
diff --git a/test/typ/math/cancel-04.out b/test/typ/math/cancel-04.out
--- a/test/typ/math/cancel-04.out
+++ b/test/typ/math/cancel-04.out
@@ -86,8 +86,7 @@
                                        text(body: [−]), 
                                        math.cancel(body: text(body: [x]), 
                                                    length: 50%, 
-                                                   stroke: (thickness: 1.1pt,
-                                                            color: rgb(100%,25%,21%,100%))) }, 
+                                                   stroke: 1.1pt + rgb(100%,25%,21%,100%)) }, 
                                numbering: none), 
                  text(body: [
 ]), 
@@ -103,7 +102,6 @@
                                                            text(body: [+]), 
                                                            text(body: [c]) }, 
                                                    length: 50%, 
-                                                   stroke: (thickness: 1.2pt,
-                                                            color: rgb(0%,45%,85%,100%))) }, 
+                                                   stroke: 1.2pt + rgb(0%,45%,85%,100%)) }, 
                                numbering: none), 
                  parbreak() })
diff --git a/test/typ/regression/pr-103-calc-quo-zero.out b/test/typ/regression/pr-103-calc-quo-zero.out
new file mode 100644
--- /dev/null
+++ b/test/typ/regression/pr-103-calc-quo-zero.out
@@ -0,0 +1,14 @@
+--- parse tree ---
+[ Comment
+, SoftBreak
+, Code
+    "typ/regression/pr-103-calc-quo-zero.typ"
+    ( line 2 , column 2 )
+    (FuncCall
+       (FieldAccess
+          (Ident (Identifier "quo")) (Ident (Identifier "calc")))
+       [ NormalArg (Literal (Int 5)) , NormalArg (Literal (Int 0)) ])
+, ParBreak
+]
+"typ/regression/pr-103-calc-quo-zero.typ" (line 2, column 2):
+Module does not have a method "quo" or division by zero
diff --git a/test/typ/regression/pr-103-calc-quo-zero.typ b/test/typ/regression/pr-103-calc-quo-zero.typ
new file mode 100644
--- /dev/null
+++ b/test/typ/regression/pr-103-calc-quo-zero.typ
@@ -0,0 +1,2 @@
+// calc.quo with a zero divisor must error, not crash.
+#calc.quo(5, 0)
diff --git a/test/typ/regression/pr-103-calc-rem-zero.out b/test/typ/regression/pr-103-calc-rem-zero.out
new file mode 100644
--- /dev/null
+++ b/test/typ/regression/pr-103-calc-rem-zero.out
@@ -0,0 +1,14 @@
+--- parse tree ---
+[ Comment
+, SoftBreak
+, Code
+    "typ/regression/pr-103-calc-rem-zero.typ"
+    ( line 2 , column 2 )
+    (FuncCall
+       (FieldAccess
+          (Ident (Identifier "rem")) (Ident (Identifier "calc")))
+       [ NormalArg (Literal (Int 5)) , NormalArg (Literal (Int 0)) ])
+, ParBreak
+]
+"typ/regression/pr-103-calc-rem-zero.typ" (line 2, column 2):
+Module does not have a method "rem" or division by zero
diff --git a/test/typ/regression/pr-103-calc-rem-zero.typ b/test/typ/regression/pr-103-calc-rem-zero.typ
new file mode 100644
--- /dev/null
+++ b/test/typ/regression/pr-103-calc-rem-zero.typ
@@ -0,0 +1,2 @@
+// calc.rem with a zero divisor must error, not crash.
+#calc.rem(5, 0)
diff --git a/test/typ/regression/pr-103-division-by-zero.out b/test/typ/regression/pr-103-division-by-zero.out
new file mode 100644
--- /dev/null
+++ b/test/typ/regression/pr-103-division-by-zero.out
@@ -0,0 +1,13 @@
+--- parse tree ---
+[ Comment
+, SoftBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/regression/pr-103-division-by-zero.typ"
+    ( line 3 , column 2 )
+    (Divided (Literal (Int 5)) (Literal (Int 0)))
+, ParBreak
+]
+"typ/regression/pr-103-division-by-zero.typ" (line 3, column 2):
+Can't / VInteger 5 and VInteger 0
diff --git a/test/typ/regression/pr-103-division-by-zero.typ b/test/typ/regression/pr-103-division-by-zero.typ
new file mode 100644
--- /dev/null
+++ b/test/typ/regression/pr-103-division-by-zero.typ
@@ -0,0 +1,3 @@
+// Integer division by zero must yield an evaluation error, not crash (uncaught
+// Haskell exception) the evaluator.
+#(5 / 0)
diff --git a/test/typ/text/deco-01.out b/test/typ/text/deco-01.out
--- a/test/typ/text/deco-01.out
+++ b/test/typ/text/deco-01.out
@@ -78,8 +78,7 @@
 There might be ]), 
                  strike(body: text(body: [redacted]), 
                         extent: 5.0e-2em, 
-                        stroke: (thickness: 10.0pt,
-                                 color: rgb(67%,80%,93%,53%))), 
+                        stroke: 10.0pt + rgb(67%,80%,93%,53%)), 
                  text(body: [ things.
 underline()]), 
                  parbreak() })
diff --git a/test/typ/visualize/curve-01.out b/test/typ/visualize/curve-01.out
--- a/test/typ/visualize/curve-01.out
+++ b/test/typ/visualize/curve-01.out
@@ -91,6 +91,5 @@
                                                 end: (50.0pt, 
                                                       0.0pt)), 
                                     curve.close(mode: "straight")), 
-                       stroke: (thickness: 2.0pt,
-                                color: rgb(100%,25%,21%,100%))), 
+                       stroke: 2.0pt + rgb(100%,25%,21%,100%)), 
                  parbreak() })
diff --git a/test/typ/visualize/shape-circle-01.out b/test/typ/visualize/shape-circle-01.out
--- a/test/typ/visualize/shape-circle-01.out
+++ b/test/typ/visualize/shape-circle-01.out
@@ -166,8 +166,7 @@
                                     body: text(body: [But, soft!])), 
                         fill: rgb(92%,32%,47%,100%), 
                         inset: 0.0pt, 
-                        stroke: (thickness: 2.0pt,
-                                 color: rgb(0%,0%,0%,100%))), 
+                        stroke: 2.0pt + rgb(0%,0%,0%,100%)), 
                  parbreak(), 
                  text(body: [Center-aligned rect in auto-sized circle.
 ]), 
diff --git a/test/typ/visualize/shape-ellipse-01.out b/test/typ/visualize/shape-ellipse-01.out
--- a/test/typ/visualize/shape-ellipse-01.out
+++ b/test/typ/visualize/shape-ellipse-01.out
@@ -176,8 +176,7 @@
                                  parbreak() }, 
                          fill: rgb(18%,80%,25%,100%), 
                          inset: 3.0pt, 
-                         stroke: (thickness: 3.0pt,
-                                  color: rgb(100%,25%,21%,100%))), 
+                         stroke: 3.0pt + rgb(100%,25%,21%,100%)), 
                  parbreak(), 
                  text(body: [An inline
 ], 
diff --git a/test/typ/visualize/shape-fill-stroke-00.out b/test/typ/visualize/shape-fill-stroke-00.out
--- a/test/typ/visualize/shape-fill-stroke-00.out
+++ b/test/typ/visualize/shape-fill-stroke-00.out
@@ -171,8 +171,7 @@
                                        body: { text(body: [6]), 
                                                text(body: [.]) }), 
                                  rect(height: 10.0pt, 
-                                      stroke: (thickness: 2.0pt,
-                                               color: rgb(13%,61%,67%,100%)), 
+                                      stroke: 2.0pt + rgb(13%,61%,67%,100%), 
                                       width: 20.0pt), 
                                  {  }, 
                                  align(alignment: horizon, 
@@ -211,8 +210,7 @@
                                                text(body: [.]) }), 
                                  rect(fill: rgb(100%,25%,21%,100%), 
                                       height: 10.0pt, 
-                                      stroke: (thickness: 2.0pt,
-                                               color: rgb(0%,0%,0%,100%)), 
+                                      stroke: 2.0pt + rgb(0%,0%,0%,100%), 
                                       width: 20.0pt), 
                                  {  }, 
                                  align(alignment: horizon, 
@@ -220,8 +218,7 @@
                                                text(body: [.]) }), 
                                  rect(fill: rgb(100%,25%,21%,100%), 
                                       height: 10.0pt, 
-                                      stroke: (thickness: 2.0pt,
-                                               color: rgb(18%,80%,25%,100%)), 
+                                      stroke: 2.0pt + rgb(18%,80%,25%,100%), 
                                       width: 20.0pt), 
                                  {  }), 
                       columns: (auto, 
diff --git a/test/typ/visualize/shape-fill-stroke-01.out b/test/typ/visualize/shape-fill-stroke-01.out
--- a/test/typ/visualize/shape-fill-stroke-01.out
+++ b/test/typ/visualize/shape-fill-stroke-01.out
@@ -119,6 +119,5 @@
 ]), 
                  box(body: square(fill: rgb(22%,80%,80%,100%), 
                                   size: 10.0pt, 
-                                  stroke: (thickness: 2.0pt,
-                                           color: rgb(0%,45%,85%,100%)))), 
+                                  stroke: 2.0pt + rgb(0%,45%,85%,100%))), 
                  parbreak() })
diff --git a/test/typ/visualize/shape-rect-01.out b/test/typ/visualize/shape-rect-01.out
--- a/test/typ/visualize/shape-rect-01.out
+++ b/test/typ/visualize/shape-rect-01.out
@@ -218,8 +218,7 @@
 ]), 
                  block(body: rect(fill: rgb(27%,70%,76%,100%), 
                                   height: 15.0pt, 
-                                  stroke: (thickness: 2.0pt,
-                                           color: rgb(13%,28%,58%,100%)))), 
+                                  stroke: 2.0pt + rgb(13%,28%,58%,100%))), 
                  parbreak(), 
                  text(body: [
 ]), 
diff --git a/test/typ/visualize/stroke-00.out b/test/typ/visualize/stroke-00.out
--- a/test/typ/visualize/stroke-00.out
+++ b/test/typ/visualize/stroke-00.out
@@ -103,8 +103,7 @@
                  text(body: [
 ]), 
                  line(length: 60.0pt, 
-                      stroke: (thickness: 1.5pt,
-                               color: rgb(0%,45%,85%,100%))), 
+                      stroke: 1.5pt + rgb(0%,45%,85%,100%)), 
                  text(body: [
 ]), 
                  v(amount: 3.0pt), 
diff --git a/test/typ/visualize/stroke-07.out b/test/typ/visualize/stroke-07.out
--- a/test/typ/visualize/stroke-07.out
+++ b/test/typ/visualize/stroke-07.out
@@ -224,8 +224,7 @@
 ]), 
                  rect(fill: rgb(0%,45%,85%,100%), 
                       height: 10.0pt, 
-                      stroke: (thickness: 0.0pt,
-                               color: rgb(100%,25%,21%,100%)), 
+                      stroke: 0.0pt + rgb(100%,25%,21%,100%), 
                       width: 10.0pt), 
                  parbreak(), 
                  line(length: 30.0pt, 
diff --git a/test/typ/visualize/stroke-08.out b/test/typ/visualize/stroke-08.out
new file mode 100644
--- /dev/null
+++ b/test/typ/visualize/stroke-08.out
@@ -0,0 +1,491 @@
+--- parse tree ---
+[ Comment
+, ParBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 3 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Plus (Literal (Numeric 2.0 Pt)) (Ident (Identifier "red")))
+                 ]))
+       , NormalArg (Ident (Identifier "red"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 4 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Plus (Literal (Numeric 2.0 Pt)) (Ident (Identifier "red")))
+                 ]))
+       , NormalArg (Literal (Numeric 2.0 Pt))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 5 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "paint") (Ident (Identifier "blue")) ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 6 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Literal (Numeric 3.0 Pt)) ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 7 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Ident (Identifier "red")) ]))
+       , NormalArg (Ident (Identifier "red"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 8 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Literal (Numeric 3.0 Pt)) ]))
+       , NormalArg (Literal (Numeric 3.0 Pt))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 9 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "paint") (Literal Auto) ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 10 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "thickness") (Literal Auto) ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 11 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict [ Reg ( Ident (Identifier "paint") , Literal Auto ) ])
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 12 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict
+                        [ Reg ( Ident (Identifier "thickness") , Literal (Numeric 2.0 Pt) )
+                        ])
+                 ]))
+       , NormalArg (Literal (Numeric 2.0 Pt))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 13 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict
+                        [ Reg ( Ident (Identifier "paint") , Ident (Identifier "blue") )
+                        , Reg ( Ident (Identifier "thickness") , Literal (Numeric 2.0 Pt) )
+                        ])
+                 ]))
+       , NormalArg (Ident (Identifier "blue"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 14 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "paint") (Ident (Identifier "red")) ])
+                 ]))
+       , NormalArg (Ident (Identifier "red"))
+       ])
+, ParBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 16 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red"))))
+       , NormalArg (Ident (Identifier "red"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 17 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red"))))
+       , NormalArg (Literal (Numeric 1.0 Pt))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 18 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (Plus (Ident (Identifier "red")) (Literal (Numeric 1.0 Pt))))
+       , NormalArg (Ident (Identifier "red"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 19 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (Plus (Ident (Identifier "red")) (Literal (Numeric 1.0 Pt))))
+       , NormalArg (Literal (Numeric 1.0 Pt))
+       ])
+, ParBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 21 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Literal (Numeric 1.0 Pt)) ])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Literal (Numeric 1.0 Pt)) ]))
+       , NormalArg (Literal (Boolean True))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 22 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Literal (Numeric 1.0 Pt)) ])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Literal (Numeric 2.0 Pt)) ]))
+       , NormalArg (Literal (Boolean False))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 23 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Literal (Numeric 1.0 Pt)) ])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg (Ident (Identifier "red")) ]))
+       , NormalArg (Literal (Boolean False))
+       ])
+, SoftBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 25 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall (Ident (Identifier "stroke")) [])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "paint") (Ident (Identifier "black")) ]))
+       , NormalArg (Literal (Boolean False))
+       ])
+, ParBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 27 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "type"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ NormalArg (Literal (Numeric 1.0 Pt)) ])
+              ])
+       , NormalArg (Literal (String "stroke"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 28 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "type"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ NormalArg (Literal (Numeric 1.0 Pt)) ])
+              ])
+       , NormalArg (Ident (Identifier "stroke"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 29 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "type"))
+              [ NormalArg
+                  (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red")))
+              ])
+       , NormalArg (Ident (Identifier "stroke"))
+       ])
+, ParBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 32 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus (Literal (Numeric 2.0 Pt)) (Ident (Identifier "red")))
+              ])
+       , NormalArg (Literal (String "2.0pt + rgb(100%,25%,21%,100%)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 33 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ NormalArg (Ident (Identifier "red")) ])
+              ])
+       , NormalArg (Literal (String "rgb(100%,25%,21%,100%)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 34 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ NormalArg (Literal (Numeric 2.0 Pt)) ])
+              ])
+       , NormalArg (Literal (String "2.0pt"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-08.typ"
+    ( line 35 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg (FuncCall (Ident (Identifier "stroke")) []) ])
+       , NormalArg (Literal (String "1pt + black"))
+       ])
+, ParBreak
+]
+--- evaluated ---
+document(body: { parbreak(), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak() })
diff --git a/test/typ/visualize/stroke-08.typ b/test/typ/visualize/stroke-08.typ
new file mode 100644
--- /dev/null
+++ b/test/typ/visualize/stroke-08.typ
@@ -0,0 +1,35 @@
+// Test the stroke type: constructor, field access, arithmetic, equality
+
+#test(stroke(2pt + red).paint, red)
+#test(stroke(2pt + red).thickness, 2pt)
+#test(stroke(paint: blue).thickness, auto)
+#test(stroke(3pt).paint, auto)
+#test(stroke(red).paint, red)
+#test(stroke(3pt).thickness, 3pt)
+#test(stroke(paint: auto).paint, auto)
+#test(stroke(thickness: auto).thickness, auto)
+#test(stroke((paint: auto)).paint, auto)
+#test(stroke((thickness: 2pt)).thickness, 2pt)
+#test(stroke((paint: blue, thickness: 2pt)).paint, blue)
+#test(stroke(stroke(paint: red)).paint, red)
+
+#test((1pt + red).paint, red)
+#test((1pt + red).thickness, 1pt)
+#test((red + 1pt).paint, red)
+#test((red + 1pt).thickness, 1pt)
+
+#test(stroke(1pt) == stroke(1pt), true)
+#test(stroke(1pt) == stroke(2pt), false)
+#test(stroke(1pt) == stroke(red), false)
+// an explicitly set field differs from auto, as in typst
+#test(stroke() == stroke(paint: black), false)
+
+#test(type(stroke(1pt)), "stroke")
+#test(type(stroke(1pt)), stroke)
+#test(type(1pt + red), stroke)
+
+// repr, matching typst's simple stroke forms
+#test(repr(2pt + red), "2.0pt + rgb(100%,25%,21%,100%)")
+#test(repr(stroke(red)), "rgb(100%,25%,21%,100%)")
+#test(repr(stroke(2pt)), "2.0pt")
+#test(repr(stroke()), "1pt + black")
diff --git a/test/typ/visualize/stroke-09.out b/test/typ/visualize/stroke-09.out
new file mode 100644
--- /dev/null
+++ b/test/typ/visualize/stroke-09.out
@@ -0,0 +1,1081 @@
+--- parse tree ---
+[ Comment
+, ParBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 4 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "cap") (Literal (String "round")) ]))
+       , NormalArg (Literal (String "round"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 5 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "join"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "join") (Literal (String "bevel")) ]))
+       , NormalArg (Literal (String "bevel"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 6 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "miter-limit"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "miter-limit") (Literal (Float 2.5)) ]))
+       , NormalArg (Literal (Float 2.5))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 7 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "miter-limit"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "miter-limit") (Literal (Int 4)) ]))
+       , NormalArg (Literal (Float 4.0))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 8 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "type"))
+              [ NormalArg
+                  (FieldAccess
+                     (Ident (Identifier "cap"))
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "round")) ]))
+              ])
+       , NormalArg (Literal (String "string"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 9 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "type"))
+              [ NormalArg
+                  (FieldAccess
+                     (Ident (Identifier "miter-limit"))
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "miter-limit") (Literal (Float 2.5)) ]))
+              ])
+       , NormalArg (Literal (String "float"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 10 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict
+                        [ Reg ( Ident (Identifier "cap") , Literal (String "square") )
+                        , Reg ( Ident (Identifier "miter-limit") , Literal (Float 2.0) )
+                        ])
+                 ]))
+       , NormalArg (Literal (String "square"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 11 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "join"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict
+                        [ Reg ( Ident (Identifier "join") , Literal (String "round") ) ])
+                 ]))
+       , NormalArg (Literal (String "round"))
+       ])
+, ParBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 14 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red")))
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 15 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "join"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red")))
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 16 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "miter-limit"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red")))
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, ParBreak
+, Comment
+, SoftBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 20 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+                 ]))
+       , NormalArg (Literal (String "round"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 21 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+                 , KeyValArg (Identifier "cap") (Literal Auto)
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 22 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "join"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict
+                        [ Reg ( Ident (Identifier "cap") , Literal (String "round") ) ])
+                 , KeyValArg (Identifier "join") (Literal (String "bevel"))
+                 ]))
+       , NormalArg (Literal (String "bevel"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 23 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict [ Reg ( Ident (Identifier "cap") , Literal Auto ) ])
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, ParBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 26 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "cap") (Literal (String "round")) ]))
+       , NormalArg (Literal (Boolean True))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 27 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+              (FuncCall (Ident (Identifier "stroke")) []))
+       , NormalArg (Literal (Boolean False))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 28 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "join") (Literal (String "bevel")) ])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "join") (Literal (String "round")) ]))
+       , NormalArg (Literal (Boolean False))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 29 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "miter-limit") (Literal (Int 4)) ])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "miter-limit") (Literal (Float 4.0)) ]))
+       , NormalArg (Literal (Boolean True))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 30 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "miter-limit") (Literal (Int 4)) ])
+              (FuncCall (Ident (Identifier "stroke")) []))
+       , NormalArg (Literal (Boolean False))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 31 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "cap") (Literal (String "round"))
+                 , KeyValArg (Identifier "join") (Literal (String "bevel"))
+                 ])
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "join") (Literal (String "bevel"))
+                 , KeyValArg (Identifier "cap") (Literal (String "round"))
+                 ]))
+       , NormalArg (Literal (Boolean True))
+       ])
+, ParBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 34 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+              ])
+       , NormalArg (Literal (String "(cap: \"round\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 35 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ KeyValArg (Identifier "join") (Literal (String "bevel")) ])
+              ])
+       , NormalArg (Literal (String "(join: \"bevel\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 36 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ KeyValArg (Identifier "miter-limit") (Literal (Float 3.0)) ])
+              ])
+       , NormalArg (Literal (String "(miter-limit: 3.0)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 37 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ KeyValArg (Identifier "miter-limit") (Literal (Int 4)) ])
+              ])
+       , NormalArg (Literal (String "(miter-limit: 4.0)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 38 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ KeyValArg (Identifier "paint") (Ident (Identifier "red"))
+                     , KeyValArg (Identifier "cap") (Literal (String "round"))
+                     ])
+              ])
+       , NormalArg
+           (Literal
+              (String "(paint: rgb(100%,25%,21%,100%), cap: \"round\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 39 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ KeyValArg (Identifier "thickness") (Literal (Numeric 2.0 Pt))
+                     , KeyValArg (Identifier "miter-limit") (Literal (Float 4.0))
+                     ])
+              ])
+       , NormalArg
+           (Literal (String "(thickness: 2.0pt, miter-limit: 4.0)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 40 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ NormalArg
+                         (Dict
+                            [ Reg ( Ident (Identifier "paint") , Ident (Identifier "red") )
+                            , Reg ( Ident (Identifier "cap") , Literal (String "round") )
+                            , Reg ( Ident (Identifier "miter-limit") , Literal (Float 2.0) )
+                            , Reg ( Ident (Identifier "thickness") , Literal (Numeric 1.0 Pt) )
+                            ])
+                     ])
+              ])
+       , NormalArg
+           (Literal
+              (String
+                 "(paint: rgb(100%,25%,21%,100%), thickness: 1.0pt, cap: \"round\", miter-limit: 2.0)"))
+       ])
+, SoftBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 42 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ NormalArg
+                         (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red")))
+                     ])
+              ])
+       , NormalArg (Literal (String "1.0pt + rgb(100%,25%,21%,100%)"))
+       ])
+, ParBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 45 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (Plus (Literal (Numeric 2.0 Pt)) (Ident (Identifier "red")))
+                     (Ident (Identifier "blue")))
+              ])
+       , NormalArg (Literal (String "2.0pt + rgb(0%,45%,85%,100%)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 46 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (Plus (Literal (Numeric 2.0 Pt)) (Ident (Identifier "red")))
+                     (Literal (Numeric 3.0 Pt)))
+              ])
+       , NormalArg (Literal (String "3.0pt + rgb(100%,25%,21%,100%)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 47 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+                     (Literal (Numeric 2.0 Pt)))
+              ])
+       , NormalArg (Literal (String "(thickness: 2.0pt, cap: \"round\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 48 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red")))
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "round")) ]))
+              ])
+       , NormalArg
+           (Literal
+              (String
+                 "(paint: rgb(100%,25%,21%,100%), thickness: 1.0pt, cap: \"round\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 49 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "join") (Literal (String "bevel")) ]))
+              ])
+       , NormalArg (Literal (String "(cap: \"round\", join: \"bevel\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 50 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "round")) ])
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "cap") (Literal (String "square")) ]))
+              ])
+       , NormalArg (Literal (String "(cap: \"square\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 51 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "paint") (Ident (Identifier "red"))
+                        , KeyValArg (Identifier "cap") (Literal (String "round"))
+                        ])
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "paint") (Ident (Identifier "blue")) ]))
+              ])
+       , NormalArg
+           (Literal (String "(paint: rgb(0%,45%,85%,100%), cap: \"round\")"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 52 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (Equals
+              (Plus
+                 (Plus (Literal (Numeric 2.0 Pt)) (Ident (Identifier "red")))
+                 (Literal (Numeric 3.0 Pt)))
+              (Plus (Literal (Numeric 3.0 Pt)) (Ident (Identifier "red"))))
+       , NormalArg (Literal (Boolean True))
+       ])
+, SoftBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 54 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (Ident (Identifier "blue"))
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ NormalArg (Literal (Numeric 2.0 Pt)) ]))
+              ])
+       , NormalArg (Literal (String "2.0pt + rgb(0%,45%,85%,100%)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 55 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (Ident (Identifier "blue"))
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ NormalArg (Literal (Numeric 2.0 Pt))
+                        , KeyValArg (Identifier "paint") (Ident (Identifier "red"))
+                        ]))
+              ])
+       , NormalArg (Literal (String "2.0pt + rgb(100%,25%,21%,100%)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 56 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (Literal (Numeric 2.0 Pt))
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ KeyValArg (Identifier "paint") (Ident (Identifier "red")) ]))
+              ])
+       , NormalArg (Literal (String "2.0pt + rgb(100%,25%,21%,100%)"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 57 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (Plus
+                     (Literal (Numeric 2.0 Pt))
+                     (FuncCall
+                        (Ident (Identifier "stroke"))
+                        [ NormalArg (Literal (Numeric 3.0 Pt))
+                        , KeyValArg (Identifier "cap") (Literal (String "round"))
+                        ]))
+              ])
+       , NormalArg (Literal (String "(thickness: 3.0pt, cap: \"round\")"))
+       ])
+, ParBreak
+, Comment
+, SoftBreak
+, Comment
+, SoftBreak
+, Comment
+, SoftBreak
+, Comment
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 63 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "paint") (Literal None) ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 64 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Plus (Literal (Numeric 1.0 Pt)) (Ident (Identifier "red")))
+                 , KeyValArg (Identifier "paint") (Literal None)
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 65 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "paint"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict [ Reg ( Ident (Identifier "paint") , Literal None ) ])
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 66 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "thickness"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict [ Reg ( Ident (Identifier "thickness") , Literal None ) ])
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 67 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg (Identifier "cap") (Literal (String "bogus")) ]))
+       , NormalArg (Literal (String "bogus"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 68 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "miter-limit"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ KeyValArg
+                     (Identifier "miter-limit") (Literal (Numeric 250.0 Percent))
+                 ]))
+       , NormalArg (Literal (Float 2.5))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 69 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict
+                        [ Reg ( Ident (Identifier "cap") , Literal (String "round") )
+                        , Reg ( Ident (Identifier "bogus") , Literal (Int 1) )
+                        ])
+                 ]))
+       , NormalArg (Literal (String "round"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 70 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FieldAccess
+              (Ident (Identifier "cap"))
+              (FuncCall
+                 (Ident (Identifier "stroke"))
+                 [ NormalArg
+                     (Dict
+                        [ Reg ( Ident (Identifier "dash") , Literal (String "dashed") ) ])
+                 ]))
+       , NormalArg (Literal Auto)
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 71 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ KeyValArg (Identifier "dash") (Literal (String "dashed")) ])
+              ])
+       , NormalArg (Literal (String "1pt + black"))
+       ])
+, SoftBreak
+, Code
+    "typ/visualize/stroke-09.typ"
+    ( line 72 , column 2 )
+    (FuncCall
+       (Ident (Identifier "test"))
+       [ NormalArg
+           (FuncCall
+              (Ident (Identifier "repr"))
+              [ NormalArg
+                  (FuncCall
+                     (Ident (Identifier "stroke"))
+                     [ NormalArg (Literal (Numeric 2.0 Pt))
+                     , KeyValArg (Identifier "dash") (Literal (String "dashed"))
+                     ])
+              ])
+       , NormalArg (Literal (String "2.0pt"))
+       ])
+, ParBreak
+]
+--- evaluated ---
+document(body: { parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak(), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 text(body: [
+]), 
+                 text(body: [✅]), 
+                 parbreak() })
diff --git a/test/typ/visualize/stroke-09.typ b/test/typ/visualize/stroke-09.typ
new file mode 100644
--- /dev/null
+++ b/test/typ/visualize/stroke-09.typ
@@ -0,0 +1,72 @@
+// Test the cap, join, and miter-limit stroke fields.
+
+// constructor and dictionary fields
+#test(stroke(cap: "round").cap, "round")
+#test(stroke(join: "bevel").join, "bevel")
+#test(stroke(miter-limit: 2.5).miter-limit, 2.5)
+#test(stroke(miter-limit: 4).miter-limit, 4.0)
+#test(type(stroke(cap: "round").cap), "string")
+#test(type(stroke(miter-limit: 2.5).miter-limit), "float")
+#test(stroke((cap: "square", miter-limit: 2.0)).cap, "square")
+#test(stroke((join: "round")).join, "round")
+
+// unset fields are auto
+#test(stroke(1pt + red).cap, auto)
+#test(stroke(1pt + red).join, auto)
+#test(stroke(1pt + red).miter-limit, auto)
+
+// a named argument overrides the base, and auto resets it; combining a
+// base with named arguments is a typst-hs extension
+#test(stroke(stroke(cap: "round")).cap, "round")
+#test(stroke(stroke(cap: "round"), cap: auto).cap, auto)
+#test(stroke((cap: "round"), join: "bevel").join, "bevel")
+#test(stroke((cap: auto)).cap, auto)
+
+// equality, including an explicitly set field vs auto
+#test(stroke(cap: "round") == stroke(cap: "round"), true)
+#test(stroke(cap: "round") == stroke(), false)
+#test(stroke(join: "bevel") == stroke(join: "round"), false)
+#test(stroke(miter-limit: 4) == stroke(miter-limit: 4.0), true)
+#test(stroke(miter-limit: 4) == stroke(), false)
+#test(stroke(cap: "round", join: "bevel") == stroke(join: "bevel", cap: "round"), true)
+
+// repr, matching typst's parenthesized stroke form
+#test(repr(stroke(cap: "round")), "(cap: \"round\")")
+#test(repr(stroke(join: "bevel")), "(join: \"bevel\")")
+#test(repr(stroke(miter-limit: 3.0)), "(miter-limit: 3.0)")
+#test(repr(stroke(miter-limit: 4)), "(miter-limit: 4.0)")
+#test(repr(stroke(paint: red, cap: "round")), "(paint: rgb(100%,25%,21%,100%), cap: \"round\")")
+#test(repr(stroke(thickness: 2pt, miter-limit: 4.0)), "(thickness: 2.0pt, miter-limit: 4.0)")
+#test(repr(stroke((paint: red, cap: "round", miter-limit: 2.0, thickness: 1pt))), "(paint: rgb(100%,25%,21%,100%), thickness: 1.0pt, cap: \"round\", miter-limit: 2.0)")
+// the simple forms still apply when these fields are unset
+#test(repr(stroke(1pt + red)), "1.0pt + rgb(100%,25%,21%,100%)")
+
+// typst-hs extension: adding to a stroke refines or merges fields
+#test(repr((2pt + red) + blue), "2.0pt + rgb(0%,45%,85%,100%)")
+#test(repr((2pt + red) + 3pt), "3.0pt + rgb(100%,25%,21%,100%)")
+#test(repr(stroke(cap: "round") + 2pt), "(thickness: 2.0pt, cap: \"round\")")
+#test(repr((1pt + red) + stroke(cap: "round")), "(paint: rgb(100%,25%,21%,100%), thickness: 1.0pt, cap: \"round\")")
+#test(repr(stroke(cap: "round") + stroke(join: "bevel")), "(cap: \"round\", join: \"bevel\")")
+#test(repr(stroke(cap: "round") + stroke(cap: "square")), "(cap: \"square\")")
+#test(repr(stroke(paint: red, cap: "round") + stroke(paint: blue)), "(paint: rgb(0%,45%,85%,100%), cap: \"round\")")
+#test((2pt + red) + 3pt == 3pt + red, true)
+// likewise with the stroke as the right operand
+#test(repr(blue + stroke(2pt)), "2.0pt + rgb(0%,45%,85%,100%)")
+#test(repr(blue + stroke(2pt, paint: red)), "2.0pt + rgb(100%,25%,21%,100%)")
+#test(repr(2pt + stroke(paint: red)), "2.0pt + rgb(100%,25%,21%,100%)")
+#test(repr(2pt + stroke(3pt, cap: "round")), "(thickness: 3.0pt, cap: \"round\")")
+
+// typst-hs leniency, accepted though typst rejects it: `none` resets a
+// field like `auto`, and any string or number works for cap, join, and
+// miter-limit. Unknown dictionary keys are ignored, and `dash` values
+// are dropped until dash is supported (typst accepts them).
+#test(stroke(paint: none).paint, auto)
+#test(stroke(1pt + red, paint: none).paint, auto)
+#test(stroke((paint: none)).paint, auto)
+#test(stroke((thickness: none)).thickness, auto)
+#test(stroke(cap: "bogus").cap, "bogus")
+#test(stroke(miter-limit: 250%).miter-limit, 2.5)
+#test(stroke((cap: "round", bogus: 1)).cap, "round")
+#test(stroke((dash: "dashed")).cap, auto)
+#test(repr(stroke(dash: "dashed")), "1pt + black")
+#test(repr(stroke(2pt, dash: "dashed")), "2.0pt")
diff --git a/typst.cabal b/typst.cabal
--- a/typst.cabal
+++ b/typst.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.4
 name:               typst
-version:            0.11.0.1
+version:            0.12
 synopsis:           Parsing and evaluating typst syntax.
 description:        A library for parsing and evaluating typst syntax.
                     Typst (<https://typst.app>) is a document layout and
