diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -134,6 +134,7 @@ app's JSON control channel (`deckcraft --control PORT`).
 
 | Date | What landed |
 |---|---|
+| 2026-10-22 | PDF export: the text layer holds what a field shows, so a slide number is searchable and copies as its number instead of the saved `‹#›` (#1153) |
 | 2026-10-23 | Chart text colour: `chart.options {textColor}` and Font Color on a selected chart colour its title, axes, legend and data labels; the colour is written to the chart part (`c:txPr` on the axes, legend, labels and chart, the title's run defaults) so an imported chart keeps its `txPr` colours through an edit and a save; text without a colour of its own takes PowerPoint's default, the theme's text colour at 65 % plus 35 % (595959 on a light theme, light grey on a dark one) |
 | 2026-10-23 | Format Background ▸ Apply to All also sets the background of the slide masters the slides use (and clears their layouts' overrides), so new slides keep it (#891) |
 | 2026-10-22 | Table text takes the master's other-text style (`otherStyle`), shapes and text boxes the presentation's default text style, as PowerPoint for Mac draws them (synthetic probe: 14 pt cells under a 14 pt `otherStyle` and a 26 pt `defaultTextStyle`); a private deck's table had been drawn 20 pt instead of 18 |
diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs
--- a/crates/pdf/src/lib.rs
+++ b/crates/pdf/src/lib.rs
@@ -303,11 +303,11 @@ pub fn master_text_pieces(pres: &Presentation, m: &deckcraft_model::Master, pape
     for pt in deckcraft_render::place_master_texts(pres, m, n) {
         for run in &pt.layout.runs {
             let Some(g) = run.glyphs.first() else { continue };
+            // The text as drawn: a slide number is its value, not the saved `‹#›` (#1153).
             let text: String = pt
-                .body
-                .paragraphs
+                .shown
                 .get(run.para)
-                .map(|p| p.runs.iter().map(|r| r.text.as_str()).collect::<String>())
+                .map(String::as_str)
                 .unwrap_or_default()
                 .chars()
                 .skip(run.chars.0)
@@ -508,11 +508,11 @@ impl Exporter<'_> {
             if run.glyphs.is_empty() || run.size <= 0.0 {
                 continue;
             }
+            // The text as drawn: a slide number is its value, not the saved `‹#›` (#1153).
             let text: String = pt
-                .body
-                .paragraphs
+                .shown
                 .get(run.para)
-                .map(|p| p.runs.iter().map(|r| r.text.as_str()).collect::<String>())
+                .map(String::as_str)
                 .unwrap_or_default()
                 .chars()
                 .skip(run.chars.0)
diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs
--- a/crates/pdf/src/tests.rs
+++ b/crates/pdf/src/tests.rs
@@ -200,8 +200,6 @@ fn unembeddable_font_returns_error_instead_of_a_pdf_missing_text() {
 #[cfg_attr(not(target_arch = "wasm32"), test)]
 fn supported_unicode_is_shaped_and_recovered_from_the_page_text_layer() {
     use deckcraft_model::{Shape, ShapeId, Xfrm};
-    use hayro_syntax::content::ops::TypedInstruction;
-    use hayro_syntax::object::{Dict, Object, Stream};
 
     let text = "Café Ελληνικά Привет";
     let face = FontDb::global().face("Ubuntu", "Regular");
@@ -223,8 +221,18 @@ fn supported_unicode_is_shaped_and_recovered_from_the_page_text_layer() {
     assert!(runs.iter().all(|run| run.face.family == "Ubuntu" && !run.glyphs.is_empty() && run.glyphs.iter().all(|glyph| glyph.0 != 0)));
 
     let bytes = export(&p, &opts(PageLayout::Slides)).unwrap();
+    let extracted = page_text(bytes, 0);
+    assert_eq!(extracted, text);
+}
+
+/// The text a PDF reader extracts from page `index` (one embedded font): the text-showing
+/// operators decoded through the font's ToUnicode map, not document metadata.
+fn page_text(bytes: Vec<u8>, index: usize) -> String {
+    use hayro_syntax::content::ops::TypedInstruction;
+    use hayro_syntax::object::{Dict, Object, Stream};
+
     let pdf = hayro_syntax::Pdf::new(bytes.clone()).unwrap();
-    let page = &pdf.pages()[0];
+    let page = &pdf.pages()[index];
     let resources = page.raw().get::<Dict<'_>>(b"Resources").unwrap();
     let fonts = resources.get::<Dict<'_>>(b"Font").unwrap();
     assert_eq!(fonts.len(), 1);
@@ -275,7 +283,55 @@ fn supported_unicode_is_shaped_and_recovered_from_the_page_text_layer() {
             _ => {}
         }
     }
-    assert_eq!(extracted, text);
+    extracted
+}
+
+/// A slide-number field reaches the text layer as the number drawn, not the saved placeholder
+/// (#1153).
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
+#[cfg_attr(not(target_arch = "wasm32"), test)]
+fn slide_number_fields_are_searchable_as_their_number() {
+    use deckcraft_model::text::{Run, RunKind};
+    use deckcraft_model::{Shape, ShapeId, Xfrm};
+
+    let mut p = deck(3);
+    for (i, slide) in p.slides.iter_mut().enumerate() {
+        let mut body = TextBody::from_text("Page ");
+        let props = body.paragraphs[0].runs[0].props.clone();
+        body.paragraphs[0].runs.push(Run { kind: RunKind::Field { field: "slidenum".into() }, text: "\u{2039}#\u{203a}".into(), props });
+        for run in &mut body.paragraphs[0].runs {
+            run.props.font = Some("Ubuntu".into());
+            run.props.size = Some(20.0);
+        }
+        Arc::make_mut(slide).shapes =
+            vec![Shape { id: ShapeId(9100 + i as u32), xfrm: Some(Xfrm::new(20.0, 20.0, 600.0, 60.0)), text: Some(body), ..Default::default() }];
+    }
+    let bytes = export(&p, &opts(PageLayout::Slides)).unwrap();
+    for i in 0..3 {
+        assert_eq!(page_text(bytes.clone(), i), format!("Page {}", i + 1));
+    }
+}
+
+/// A line break (Shift+Enter) inside a paragraph doesn't shift the text after it in the text layer:
+/// the layout counts it as a character, and so does the layer's text now.
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
+#[cfg_attr(not(target_arch = "wasm32"), test)]
+fn a_line_break_keeps_the_text_layer_in_step() {
+    use deckcraft_model::text::{Run, RunKind};
+    use deckcraft_model::{Shape, ShapeId, Xfrm};
+
+    let mut body = TextBody::from_text("one");
+    let props = body.paragraphs[0].runs[0].props.clone();
+    body.paragraphs[0].runs.push(Run { kind: RunKind::Break, text: String::new(), props: props.clone() });
+    body.paragraphs[0].runs.push(Run { kind: RunKind::Text, text: "two".into(), props });
+    for run in &mut body.paragraphs[0].runs {
+        run.props.font = Some("Ubuntu".into());
+        run.props.size = Some(20.0);
+    }
+    let mut p = deck(1);
+    Arc::make_mut(&mut p.slides[0]).shapes =
+        vec![Shape { id: ShapeId(9200), xfrm: Some(Xfrm::new(20.0, 20.0, 600.0, 120.0)), text: Some(body), ..Default::default() }];
+    assert_eq!(page_text(export(&p, &opts(PageLayout::Slides)).unwrap(), 0), "onetwo");
 }
 
 /// Notes pages and handouts print on the chosen paper, landscape included, with every slide box
diff --git a/crates/render/src/placed.rs b/crates/render/src/placed.rs
--- a/crates/render/src/placed.rs
+++ b/crates/render/src/placed.rs
@@ -14,7 +14,11 @@ pub struct PlacedText {
     pub layout: TextLayout,
     /// Shape-local → slide points (text rotation included, flips not: see [`crate::text_transform`]).
     pub transform: Affine,
+    /// The text as saved. Its character offsets are not `layout`'s where a field or a line break
+    /// is drawn differently: read the text the runs index into from `shown`.
     pub body: TextBody,
+    /// Each paragraph's text as laid out, fields evaluated: what `layout`'s runs index into.
+    pub shown: Vec<String>,
 }
 
 /// A clickable area: a shape action or a text hyperlink.
@@ -162,7 +166,8 @@ fn place_text(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, owner: ShapeId
         let r = Rect::new(x0, line.top, x1, line.bottom);
         out.links.push(PlacedLink { rect: m.transform_rect_bbox(r), link, shape: owner, text: true });
     }
-    out.texts.push(PlacedText { layout, transform: m, body: body.clone() });
+    let shown = deckcraft_text::shown_paragraphs(rctx, s, body, fields);
+    out.texts.push(PlacedText { layout, transform: m, body: body.clone(), shown });
 }
 
 /// The hyperlink of the model run containing character `at` of paragraph `para`.
diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs
--- a/crates/text/src/lib.rs
+++ b/crates/text/src/lib.rs
@@ -700,6 +700,25 @@ pub fn autonum(scheme: &str, n: u32) -> String {
     }
 }
 
+/// The characters run `run` lays out: a field's current value (in the run's language `lang`), a
+/// line break as `\u{b}`, otherwise its text.
+fn shown_run(run: &deckcraft_model::text::Run, lang: Option<&str>, fields: &dyn Fields) -> String {
+    match &run.kind {
+        RunKind::Text | RunKind::Math { .. } => run.text.clone(),
+        RunKind::Break => "\u{b}".into(),
+        RunKind::Field { field } => fields.field_in(field, lang).unwrap_or_else(|| run.text.clone()),
+    }
+}
+
+/// Each paragraph's text as [`layout`] lays it out, fields evaluated: the text that
+/// [`GlyphRun::chars`] index into (#1153).
+pub fn shown_paragraphs(ctx: &Ctx, shape: &Shape, body: &TextBody, fields: &dyn Fields) -> Vec<String> {
+    body.paragraphs
+        .iter()
+        .map(|para| para.runs.iter().map(|run| shown_run(run, resolve::run(ctx, shape, para, &run.props).lang.as_deref(), fields)).collect())
+        .collect()
+}
+
 #[allow(clippy::too_many_arguments)]
 fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, opts: &Opts, number: Option<u32>, vertical: Upright) -> ParaShaped {
     let db = FontDb::global();
@@ -729,12 +748,7 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64,
     for run in &para.runs {
         let rp = resolve::run(ctx, shape, para, &run.props);
         let st = make_style(ctx, &rp, scale, opts.prompt_color);
-        let t: String = match &run.kind {
-            RunKind::Text => run.text.clone(),
-            RunKind::Break => "\u{b}".into(),
-            RunKind::Field { field } => opts.fields.field_in(field, rp.lang.as_deref()).unwrap_or_else(|| run.text.clone()),
-            RunKind::Math { .. } => run.text.clone(),
-        };
+        let t = shown_run(run, rp.lang.as_deref(), opts.fields);
         let si = styles.len();
         styles.push(st);
         char_style.extend(std::iter::repeat_n(si, t.chars().count()));
