diff --git a/docs/format/text-variables.md b/docs/format/text-variables.md
--- a/docs/format/text-variables.md
+++ b/docs/format/text-variables.md
@@ -45,6 +45,7 @@ chunk with it (their settings are all reproduced, below).
 | 0xCAAA | `MatchParagraphStyleType` | 135 | `MatchParagraphStylePreference` |
 | 0xCAAB | `ModificationDateType` | 135 | `DateVariablePreference` |
 | 0xCAAC | `CreationDateType` | 135 | `DateVariablePreference` |
+| 0xCAAD | `MatchCharacterStyleType` | 282 (privately held pairs, below) | `MatchCharacterStylePreference` |
 | 0xCAB5 | `XrefPageNumberType` | 135 | none |
 | 0xCAB6 | `XrefChapterNumberType` | 135 | none |
 | 0xCAC0 | `LiveCaptionType` | 130 | `CaptionMetadataVariablePreference` |
@@ -88,6 +89,20 @@ settings is a paragraph style UID. It differs between files, but in all
   0 or 1 except in 2 definitions, in files without IDML, where it is
   0x40. That may be a flag bit for this attribute; which one is open, so
   those definitions get no `DeleteEndPunctuation`.
+- Character style match (0xCAAD): the fields are those of the running
+  header. *b* is the character style UID, written as
+  `AppliedCharacterStyle`; *c* is `SearchStrategy`, 0 `FirstOnPage`,
+  1 `LastOnPage`; *a* is `ChangeCase`, 0 or 0xCAC9 `None`. The
+  `MatchCharacterStylePreference` element has the same attributes as
+  `MatchParagraphStylePreference` with `AppliedCharacterStyle` in place of
+  `AppliedParagraphStyle`. The type table above was drawn from the public
+  corpus and lacks this code. Over 282 pairs held privately (826
+  definitions), the converter's `TextVariable` element, including
+  its settings element, equals the reference IDML's in 282 of 282 pairs.
+  The values seen are `FirstOnPage`, `None`, `DeleteEndPunctuation="false"`
+  and an empty `TextBefore` and `TextAfter`; a variable with another
+  value in *a* or *c* has not been seen, so those mappings are those of
+  the running header and not independently proven.
 - Chapter number (0xCAA9): *a* is `Format`, 0 `Current`; 0x4C15
   `Arabic` (one definition, in a trustworthy pair).
 
@@ -156,6 +171,13 @@ In the corpus of 2026-10 two more kinds of instance show their text:
 
 - A running header on a master page shows `TextBefore`, `<`, the
   variable's name, `>` and `TextAfter`.
+- A character style match on a master page shows the same as a running
+  header there: `TextBefore`, `<`, the variable's name, `>` and
+  `TextAfter`. On a page, the instance shows the text found, or the
+  same placeholder when nothing matches; that depends on the layout, so
+  no `ResultText` is written for it. Over the 282 pairs above the
+  converter writes `ResultText` for 3,026 instances, all equal to the
+  reference, and leaves out 883 on pages, where the reference has one.
 - A chapter number in the `Current` format shows `TextBefore`, the
   document's chapter number (`ChapterNumberPreference`) and
   `TextAfter`.
diff --git a/src/idml/designmap.rs b/src/idml/designmap.rs
--- a/src/idml/designmap.rs
+++ b/src/idml/designmap.rs
@@ -1322,6 +1322,38 @@ impl Writer<'_> {
                 }
                 x.end();
             }
+            0xCAAD => {
+                x.start("MatchCharacterStylePreference");
+                text(x, "TextBefore");
+                text(x, "TextAfter");
+                x.attr("AppliedCharacterStyle", self.style_ref(v.style, false));
+                if let Some(st) = &v.settings {
+                    match st.c {
+                        0 => {
+                            x.attr("SearchStrategy", "FirstOnPage");
+                        }
+                        1 => {
+                            x.attr("SearchStrategy", "LastOnPage");
+                        }
+                        _ => {}
+                    }
+                    match st.a {
+                        0 | 0xCAC9 => {
+                            x.attr("ChangeCase", "None");
+                        }
+                        0xCAD0 => {
+                            x.attr("ChangeCase", "Sentencecase");
+                        }
+                        _ => {}
+                    }
+                }
+                // Every corpus sample is `false`; no field is known for it
+                // (text-variables.md, running headers).
+                if same || v.settings.as_ref().is_some_and(|st| st.c <= 1) {
+                    x.attr("DeleteEndPunctuation", "false");
+                }
+                x.end();
+            }
             // Chapter numbers: the format code is the first u32
             // (text-variables.md, settings).
             0xCAA9 if v.settings.is_some() => {
@@ -1421,7 +1453,7 @@ impl Writer<'_> {
                     .unwrap_or(&self.name)
                     .to_string(),
             ),
-            Some(d) if d.kind == 0xCAAA && self.on_master(story) => {
+            Some(d) if matches!(d.kind, 0xCAAA | 0xCAAD) && self.on_master(story) => {
                 around(d, &format!("<{}>", d.name))
             }
             Some(d) if d.kind == 0xCAA9 && d.settings.as_ref().is_some_and(|s| s.a == 0) => self
diff --git a/src/model/variable.rs b/src/model/variable.rs
--- a/src/model/variable.rs
+++ b/src/model/variable.rs
@@ -20,6 +20,7 @@ pub const TYPES: &[(u32, &str)] = &[
     (0xCAA9, "ChapterNumberType"),
     (0xCAAA, "MatchParagraphStyleType"),
     (0xCAAB, "ModificationDateType"),
+    (0xCAAD, "MatchCharacterStyleType"),
     (0xCAAC, "CreationDateType"),
     (0xCAB5, "XrefPageNumberType"),
     (0xCAB6, "XrefChapterNumberType"),
@@ -35,7 +36,8 @@ pub struct TextVariable {
     /// The date format of date variables; the contents of custom text
     /// variables (not proven, see the format notes).
     pub text: String,
-    /// Paragraph style of a running header (`MatchParagraphStyleType`).
+    /// Paragraph style of a running header (`MatchParagraphStyleType`), or
+    /// the character style a `MatchCharacterStyleType` variable follows.
     pub style: Option<u32>,
     /// The fields not identified hold the values every corpus sample of
     /// this type has, so the IDML settings are those of the samples.
@@ -112,7 +114,7 @@ fn parse(enc: Encoding, uid: u32, data: &[u8]) -> Result<TextVariable, Error> {
                 (zero(&rest[..4]) || enc.u32_at(rest, 0) == Some(0xCAB3)) && zero(&rest[4..])
             }
             0xCAC0 => enc.u32_at(rest, 0) == Some(0x8C64) && zero(&rest[4..]),
-            0xCAAA => {
+            0xCAAA | 0xCAAD => {
                 style = enc.u32_at(rest, 4);
 
                 zero(&rest[..4]) && zero(&rest[8..])
@@ -200,4 +202,20 @@ mod tests {
                 .as_in_samples
         );
     }
+
+    #[test]
+    fn reads_the_character_style_of_a_match_variable() {
+        let mut d = counted_text("Header");
+        d.extend(counted_text(""));
+        d.extend(0xCAADu32.to_le_bytes());
+        d.extend(0xCAC9u32.to_le_bytes());
+        d.extend(0x81u32.to_le_bytes());
+        // *c*, then the empty texts before and after.
+        d.extend([0; 12]);
+        let v = parse(crate::object::Encoding::default(), 0x8A, &d).unwrap();
+        assert_eq!(v.type_name(), Some("MatchCharacterStyleType"));
+        assert_eq!(v.style, Some(0x81));
+        let s = v.settings.unwrap();
+        assert_eq!((s.a, s.b, s.c), (0xCAC9, 0x81, 0));
+    }
 }
