diff --git a/Cargo.toml b/Cargo.toml
index bc0f991f41e50830352aaf1db72eb9209ff6da5a..0faaa6cc751f8caea07d8aa663858d35d7266650 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,8 @@ license = "EPL-2.0"
 anyhow = "1.0"
 camino = "1.1"
 clap = { version = "4.4", features = ["derive"] }
+fontconfig = "0.8"
+harfbuzz_rs = "2.0"
 indexmap = "2.2"
 rational = "1.5"
 serde = { version = "1.0.188", features = ["derive"] }
diff --git a/question.png b/question.png
new file mode 100644
index 0000000000000000000000000000000000000000..41726245a741c11e3e674d01ca716ae8ee68aafa
Binary files /dev/null and b/question.png differ
diff --git a/src/iotro.rs b/src/iotro.rs
index 94eca890fea03cc148b68ab3451d54a487fd8277..98b5a50c822643ea09ab8eb6b2660ba3288ea4fe 100644
--- a/src/iotro.rs
+++ b/src/iotro.rs
@@ -7,7 +7,7 @@ use std::{
 	str::FromStr
 };
 use svgwriter::{
-	tags::{Group, Rect, TagWithPresentationAttributes, Text},
+	tags::{Group, Rect, TagWithPresentationAttributes as _, Text},
 	Graphic
 };
 
@@ -134,14 +134,14 @@ impl Debug for Language<'_> {
 }
 
 #[repr(u16)]
-enum FontSize {
+pub(crate) enum FontSize {
 	Huge = 72,
 	Large = 56,
 	Big = 44
 }
 
 #[repr(u16)]
-enum FontWeight {
+pub(crate) enum FontWeight {
 	Normal = 400,
 	SemiBold = 500,
 	Bold = 700
diff --git a/src/main.rs b/src/main.rs
index 6c9a7ee049c869ec2d80930e04db1e31d6d159cd..af935792982b4418f7f06f4b3045914b5bb407fc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,16 +3,18 @@
 #![forbid(elided_lifetimes_in_paths, unsafe_code)]
 
 mod iotro;
+mod question;
 mod render;
 mod time;
 
-use crate::{
+use self::{
+	iotro::Language,
+	question::Question,
 	render::{ffmpeg::FfmpegOutputFormat, Renderer},
 	time::{parse_date, parse_time, Date, Time}
 };
 use camino::Utf8PathBuf as PathBuf;
 use clap::Parser;
-use iotro::Language;
 use rational::Rational;
 use serde::{Deserialize, Serialize};
 use serde_with::{serde_as, DisplayFromStr};
diff --git a/src/question.rs b/src/question.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f38a8b5c3aacfc9b6e5a17a6b1dc583d5e28231d
--- /dev/null
+++ b/src/question.rs
@@ -0,0 +1,160 @@
+use crate::{iotro::Language, Resolution};
+use fontconfig::Fontconfig;
+use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
+use std::sync::OnceLock;
+use svgwriter::{
+	tags::{Group, Path, Rect, TSpan, TagWithPresentationAttributes as _, Text},
+	Data, Graphic, Transform
+};
+
+pub(crate) struct Question {
+	res: Resolution,
+	g: Group
+}
+
+impl Question {
+	pub(crate) fn new(res: Resolution, lang: &Language<'_>, str: String) -> Self {
+		static FONT: OnceLock<Owned<Font<'static>>> = OnceLock::new();
+		let font = FONT.get_or_init(|| {
+			let fc = Fontconfig::new().unwrap();
+			let font_path = fc.find("Noto Sans", None).unwrap().path;
+			let face = Face::from_file(font_path, 0).unwrap();
+			Font::new(face)
+		});
+		let upem = font.face().upem();
+
+		// constants
+		let border_r = 12;
+		let font_size = 44;
+		let line_height = font_size * 6 / 5;
+		let padding = font_size / 2;
+		let margin_x = 240;
+		let margin_y = padding * 3 /2;
+		let question_offset = 64;
+		let question_width = 240;
+
+		// calculated
+		let box_width = 1920 - 2 * margin_x;
+		let text_width = box_width - 2 * padding;
+
+		// calculates the width of the given string
+		let width_of = |s: &str| {
+			let width: i32 =
+				harfbuzz_rs::shape(font, UnicodeBuffer::new().add_str(s), &[])
+					.get_glyph_positions()
+					.iter()
+					.map(|glyph_pos| glyph_pos.x_advance)
+					.sum();
+			(width * font_size) / upem as i32
+		};
+		let space_width = width_of(" ");
+
+		// lay out the text
+		let mut text = Text::new()
+			.with_dominant_baseline("hanging")
+			.with_transform(
+				Transform::new().translate(padding, padding + font_size / 2 + border_r)
+			);
+		let words = str.split_whitespace();
+		let mut text_height = 0;
+		let mut text_x = 0;
+		for word in words {
+			let word_width = width_of(word);
+			if text_x + word_width > text_width {
+				text_x = 0;
+				text_height += line_height;
+			}
+			text.push(
+				TSpan::new()
+					.with_x(text_x)
+					.with_y(text_height)
+					.append(word.to_owned())
+			);
+			text_x += word_width + space_width;
+		}
+		text_height += font_size;
+
+		// calculated
+		let box_height = text_height + 2 * padding + font_size / 2 + border_r;
+
+		let mut g = Group::new()
+			.with_fill("white")
+			.with_font_family("Noto Sans")
+			.with_font_size(font_size)
+			.with_transform(
+				Transform::new().translate(margin_x, 1080 - margin_y - box_height)
+			);
+
+		let mut outline = Data::new();
+		outline.move_by(border_r, 0).horiz_line_to(question_offset);
+		outline
+			.vert_line_by(-font_size / 2)
+			.arc_by(border_r, border_r, 0, false, true, border_r, -border_r)
+			.horiz_line_by(question_width)
+			.arc_by(border_r, border_r, 0, false, true, border_r, border_r)
+			.vert_line_by(font_size)
+			.arc_by(border_r, border_r, 0, false, true, -border_r, border_r)
+			.horiz_line_by(-question_width)
+			.arc_by(border_r, border_r, 0, false, true, -border_r, -border_r)
+			.vert_line_by(-font_size / 2)
+			.move_by(question_width + 2 * border_r, 0);
+		outline
+			.horiz_line_to(box_width - border_r)
+			.arc_by(border_r, border_r, 0, false, true, border_r, border_r)
+			.vert_line_by(box_height - 2 * border_r)
+			.arc_by(border_r, border_r, 0, false, true, -border_r, border_r)
+			.horiz_line_to(border_r)
+			.arc_by(border_r, border_r, 0, false, true, -border_r, -border_r)
+			.vert_line_to(border_r)
+			.arc_by(border_r, border_r, 0, false, true, border_r, -border_r);
+		g.push(
+			Path::new()
+				.with_stroke("#fff")
+				.with_stroke_width(3)
+				.with_fill("#000")
+				.with_fill_opacity(".3")
+				.with_d(outline)
+		);
+		g.push(
+			Text::new()
+				.with_x(question_offset + question_width / 2 + border_r)
+				.with_y(0)
+				.with_dominant_baseline("middle")
+				.with_text_anchor("middle")
+				.with_font_weight(600)
+				.append("Question")
+		);
+		g.push(text);
+
+		Self { res, g }
+	}
+
+	pub(crate) fn finish(self) -> Graphic {
+		let mut svg = Graphic::new();
+		svg.set_width(self.res.width());
+		svg.set_height(self.res.height());
+		svg.set_view_box("0 0 1920 1080");
+		svg.push(
+			Rect::new()
+				.with_fill("#040")
+				.with_x(0)
+				.with_y(0)
+				.with_width(1920)
+				.with_height(1080)
+		);
+		svg.push(self.g);
+		svg
+	}
+}
+
+#[cfg(test)]
+#[test]
+fn question() {
+	let svg = Question::new(
+		Resolution::FullHD,
+		&Language::default(),
+		"Hallo Welt! Dies ist eine sehr kluge Frage aus dem Publikum. Die Frage ist nämlich: Was ist eigentlich die Frage?".into()
+	)
+	.finish();
+	std::fs::write("question.svg", svg.to_string_pretty()).unwrap();
+}
diff --git a/tmp.sh b/tmp.sh
new file mode 100755
index 0000000000000000000000000000000000000000..df71030a579b6cace094c34b9acdda24633b5cdc
--- /dev/null
+++ b/tmp.sh
@@ -0,0 +1,15 @@
+#!/bin/busybox ash
+set -euo pipefail
+
+rm tmp.mkv || true
+
+ffmpeg -hide_banner \
+	-loop 1 -r 25 -t 4 -i question.png \
+	-filter_complex "
+		gradients=s=2560x1440:d=4:c0=#000055:c1=#005500:x0=480:y0=540:x1=1440:y1=540[input];
+		[0]fade=t=in:st=0:d=1:alpha=1,fade=t=out:st=3:d=1:alpha=1[overlay];
+		[input][overlay]overlay=eval=frame:x=0:y=0[v]
+	" \
+	-map "[v]" \
+	-c:v libsvtav1 -preset 1 -crf 18 \
+	"tmp.mkv"