diff --git a/data/assets/diff-byd.png b/data/assets/diff-byd.png
deleted file mode 100644
index 22b9eff..0000000
Binary files a/data/assets/diff-byd.png and /dev/null differ
diff --git a/data/assets/diff-etr.png b/data/assets/diff-etr.png
deleted file mode 100644
index 1671368..0000000
Binary files a/data/assets/diff-etr.png and /dev/null differ
diff --git a/data/assets/diff-ftr.png b/data/assets/diff-ftr.png
deleted file mode 100644
index af5b3d5..0000000
Binary files a/data/assets/diff-ftr.png and /dev/null differ
diff --git a/data/assets/diff-prs.png b/data/assets/diff-prs.png
deleted file mode 100644
index 0265965..0000000
Binary files a/data/assets/diff-prs.png and /dev/null differ
diff --git a/data/assets/diff-pst.png b/data/assets/diff-pst.png
deleted file mode 100644
index e57182f..0000000
Binary files a/data/assets/diff-pst.png and /dev/null differ
diff --git a/data/assets/grade_background.png b/data/assets/grade_background.png
new file mode 100644
index 0000000..35468b4
Binary files /dev/null and b/data/assets/grade_background.png differ
diff --git a/data/assets/name_background.png b/data/assets/name_background.png
new file mode 100644
index 0000000..dd50a1f
Binary files /dev/null and b/data/assets/name_background.png differ
diff --git a/data/assets/placeholder-jacket.jpg b/data/assets/placeholder_jacket.jpg
similarity index 100%
rename from data/assets/placeholder-jacket.jpg
rename to data/assets/placeholder_jacket.jpg
diff --git a/data/assets/ptt_emblem.png b/data/assets/ptt_emblem.png
new file mode 100644
index 0000000..1106fbc
Binary files /dev/null and b/data/assets/ptt_emblem.png differ
diff --git a/data/assets/score_background.png b/data/assets/score_background.png
new file mode 100644
index 0000000..08d7cd9
Binary files /dev/null and b/data/assets/score_background.png differ
diff --git a/data/assets/status_background.png b/data/assets/status_background.png
new file mode 100644
index 0000000..e0c2ef9
Binary files /dev/null and b/data/assets/status_background.png differ
diff --git a/data/assets/top_background.png b/data/assets/top_background.png
new file mode 100644
index 0000000..a4d4f37
Binary files /dev/null and b/data/assets/top_background.png differ
diff --git a/src/assets.rs b/src/assets.rs
index 1f76257..aba0dd8 100644
--- a/src/assets.rs
+++ b/src/assets.rs
@@ -2,28 +2,34 @@
 use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock};
 
 use freetype::{Face, Library};
+use image::{imageops::FilterType, ImageBuffer, Rgb, Rgba};
+
+use crate::chart::Difficulty;
 
 #[inline]
-fn get_data_dir() -> PathBuf {
+pub fn get_data_dir() -> PathBuf {
 	PathBuf::from_str(&var("SHIMMERING_DATA_DIR").expect("Missing `SHIMMERING_DATA_DIR` env var"))
 		.expect("`SHIMMERING_DATA_DIR` is not a valid path")
 }
 
 #[inline]
-fn get_font(name: &str, assets_dir: &PathBuf) -> RefCell<Face> {
+pub fn get_assets_dir() -> PathBuf {
+	get_data_dir().join("assets")
+}
+
+#[inline]
+fn get_font(name: &str) -> RefCell<Face> {
 	let face = FREETYPE_LIB.with(|lib| {
-		lib.new_face(assets_dir.join(format!("{}-variable.ttf", name)), 0)
+		lib.new_face(get_assets_dir().join(format!("{}-variable.ttf", name)), 0)
 			.expect(&format!("Could not load {} font", name))
 	});
 	RefCell::new(face)
 }
 
 thread_local! {
-pub static DATA_DIR: PathBuf = get_data_dir();
-pub static ASSETS_DIR: PathBuf = DATA_DIR.with(|p| p.join("assets"));
 pub static FREETYPE_LIB: Library = Library::init().unwrap();
-pub static SAIRA_FONT: RefCell<Face> = ASSETS_DIR.with(|assets_dir| get_font("saira", assets_dir));
-pub static EXO_FONT: RefCell<Face> = ASSETS_DIR.with(|assets_dir| get_font("exo", assets_dir));
+pub static SAIRA_FONT: RefCell<Face> = get_font("saira");
+pub static EXO_FONT: RefCell<Face> = get_font("exo");
 }
 
 #[inline]
@@ -31,3 +37,100 @@ pub fn should_skip_jacket_art() -> bool {
 	static CELL: OnceLock<bool> = OnceLock::new();
 	*CELL.get_or_init(|| var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1")
 }
+
+pub fn get_b30_background() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg"))
+			.expect("Could not open b30 background");
+
+		raw_b30_background
+			.resize(
+				3 * raw_b30_background.width(),
+				3 * raw_b30_background.height(),
+				FilterType::Lanczos3,
+			)
+			.blur(7.0)
+			.into_rgb8()
+	})
+}
+
+pub fn get_count_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		image::open(get_assets_dir().join("count_background.png"))
+			.expect("Could not open count background")
+			.into_rgba8()
+	})
+}
+
+pub fn get_score_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		image::open(get_assets_dir().join("score_background.png"))
+			.expect("Could not open score background")
+			.into_rgba8()
+	})
+}
+
+pub fn get_status_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		image::open(get_assets_dir().join("status_background.png"))
+			.expect("Could not open status background")
+			.into_rgba8()
+	})
+}
+
+pub fn get_grade_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		image::open(get_assets_dir().join("grade_background.png"))
+			.expect("Could not open grade background")
+			.into_rgba8()
+	})
+}
+
+pub fn get_top_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		image::open(get_assets_dir().join("top_background.png"))
+			.expect("Could not open top background")
+			.into_rgb8()
+	})
+}
+
+pub fn get_name_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		image::open(get_assets_dir().join("name_background.png"))
+			.expect("Could not open name background")
+			.into_rgb8()
+	})
+}
+
+pub fn get_ptt_emblem() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
+	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		image::open(get_assets_dir().join("ptt_emblem.png"))
+			.expect("Could not open ptt emblem")
+			.into_rgba8()
+	})
+}
+
+pub fn get_difficulty_background(
+	difficulty: Difficulty,
+) -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
+	static CELL: OnceLock<[ImageBuffer<Rgba<u8>, Vec<u8>>; 5]> = OnceLock::new();
+	&CELL.get_or_init(|| {
+		let assets_dir = get_assets_dir();
+		Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| {
+			image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase())))
+				.expect(&format!(
+					"Could not get background for difficulty {:?}",
+					shorthand
+				))
+				.into_rgba8()
+		})
+	})[difficulty.to_index()]
+}
diff --git a/src/bitmap.rs b/src/bitmap.rs
index c3a96e2..9aebb4d 100644
--- a/src/bitmap.rs
+++ b/src/bitmap.rs
@@ -2,15 +2,103 @@ use freetype::{
 	bitmap::PixelMode,
 	face::{KerningMode, LoadFlag},
 	ffi::{FT_Err_Ok, FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS},
-	Face, FtResult, Stroker, StrokerLineCap, StrokerLineJoin,
+	Bitmap, BitmapGlyph, Face, FtResult, Glyph, StrokerLineCap, StrokerLineJoin,
 };
+use image::GenericImage;
 use num::traits::Euclid;
 
 use crate::{assets::FREETYPE_LIB, context::Error};
 
-// {{{ Config types
-pub type Color = (u8, u8, u8, u8);
+// {{{ Color
+#[derive(Debug, Clone, Copy)]
+pub struct Color(pub u8, pub u8, pub u8, pub u8);
 
+impl Color {
+	pub const BLACK: Self = Self::from_rgb_int(0x000000);
+	pub const WHITE: Self = Self::from_rgb_int(0xffffff);
+
+	#[inline]
+	pub const fn from_rgba_int(i: u32) -> Self {
+		Self(
+			(i >> 24) as u8,
+			((i >> 16) & 0xff) as u8,
+			((i >> 8) & 0xff) as u8,
+			(i & 0xff) as u8,
+		)
+	}
+
+	#[inline]
+	pub const fn from_rgb_int(i: u32) -> Self {
+		Self::from_rgba_int((i << 8) + 0xff)
+	}
+
+	#[inline]
+	pub fn alpha(mut self, a: u8) -> Self {
+		self.3 = a;
+		self
+	}
+}
+// }}}
+// {{{ Rect
+#[derive(Debug, Clone, Copy)]
+pub struct Rect {
+	pub x: i32,
+	pub y: i32,
+	pub width: u32,
+	pub height: u32,
+}
+
+impl Rect {
+	#[inline]
+	pub fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
+		Self {
+			x,
+			y,
+			width,
+			height,
+		}
+	}
+
+	#[inline]
+	pub fn from_extremes(x_min: i32, y_min: i32, x_max: i32, y_max: i32) -> Self {
+		Self::new(x_min, y_min, (x_max - x_min) as u32, (y_max - y_min) as u32)
+	}
+
+	#[inline]
+	pub fn from_image(image: &impl GenericImage) -> Self {
+		Self::new(0, 0, image.width(), image.height())
+	}
+
+	#[inline]
+	pub fn align(&self, alignment: (Align, Align), pos: Position) -> Position {
+		(
+			pos.0 - alignment.0.scale(self.width) as i32,
+			pos.1 - alignment.1.scale(self.height) as i32,
+		)
+	}
+
+	#[inline]
+	pub fn align_whole(&self, alignment: (Align, Align), pos: Position) -> Self {
+		let pos = self.align(alignment, pos);
+		Self::new(pos.0, pos.1, self.width, self.height)
+	}
+
+	#[inline]
+	pub fn center(&self) -> Position {
+		(
+			self.x + self.width as i32 / 2,
+			self.y + self.height as i32 / 2,
+		)
+	}
+
+	#[inline]
+	pub fn top_left(&self) -> Position {
+		(self.x, self.y)
+	}
+}
+// }}}
+// {{{ Align
+#[allow(dead_code)]
 #[derive(Debug, Clone, Copy)]
 pub enum Align {
 	Start,
@@ -18,13 +106,32 @@ pub enum Align {
 	End,
 }
 
+impl Align {
+	#[inline]
+	pub fn scale(self, dist: u32) -> u32 {
+		match self {
+			Self::Start => 0,
+			Self::Center => dist / 2,
+			Self::End => dist,
+		}
+	}
+}
+// }}}
+// {{{ Other types
+pub type Position = (i32, i32);
+
+fn float_to_ft_fixed(f: f32) -> i64 {
+	(f * 64.0) as i64
+}
+
 #[derive(Debug, Clone, Copy)]
 pub struct TextStyle {
 	pub size: u32,
 	pub weight: u32,
 	pub color: Color,
-	pub h_align: Align,
-	pub v_align: Align,
+	pub align: (Align, Align),
+	pub stroke: Option<(Color, f32)>,
+	pub drop_shadow: Option<(Color, Position)>,
 }
 // }}}
 // {{{ BitmapCanvas
@@ -48,7 +155,7 @@ impl BitmapCanvas {
 	// }}}
 	// {{{ Draw RBG image
 	/// Draws a bitmap image
-	pub fn blit_rbg(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), src: &[u8]) {
+	pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
 		let height = self.buffer.len() as u32 / 3 / self.width;
 		for dx in 0..iw {
 			for dy in 0..ih {
@@ -59,7 +166,7 @@ impl BitmapCanvas {
 					let g = src[(dx + dy * iw) as usize * 3 + 1];
 					let b = src[(dx + dy * iw) as usize * 3 + 2];
 
-					let color = (r, g, b, 255);
+					let color = Color(r, g, b, 0xff);
 
 					self.set_pixel((x as u32, y as u32), color);
 				}
@@ -69,7 +176,7 @@ impl BitmapCanvas {
 	// }}}
 	// {{{ Draw RGBA image
 	/// Draws a bitmap image taking care of the alpha channel.
-	pub fn blit_rbga(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), src: &[u8]) {
+	pub fn blit_rbga(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
 		let height = self.buffer.len() as u32 / 3 / self.width;
 		for dx in 0..iw {
 			for dy in 0..ih {
@@ -81,7 +188,7 @@ impl BitmapCanvas {
 					let b = src[(dx + dy * iw) as usize * 4 + 2];
 					let a = src[(dx + dy * iw) as usize * 4 + 3];
 
-					let color = (r, g, b, a);
+					let color = Color(r, g, b, a);
 
 					self.set_pixel((x as u32, y as u32), color);
 				}
@@ -91,7 +198,7 @@ impl BitmapCanvas {
 	// }}}
 	// {{{ Fill
 	/// Fill with solid color
-	pub fn fill(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), color: Color) {
+	pub fn fill(&mut self, pos: Position, (iw, ih): (u32, u32), color: Color) {
 		let height = self.buffer.len() as u32 / 3 / self.width;
 		for dx in 0..iw {
 			for dy in 0..ih {
@@ -105,15 +212,13 @@ impl BitmapCanvas {
 	}
 	// }}}
 	// {{{ Draw text
-	// TODO: perform gamma correction on the color interpolation.
-	/// Render text
-	pub fn text(
+	pub fn plan_text_rendering(
 		&mut self,
-		pos: (i32, i32),
+		pos: Position,
 		face: &mut Face,
 		style: TextStyle,
 		text: &str,
-	) -> Result<(), Error> {
+	) -> Result<(Position, Rect, Vec<(i64, Glyph)>), Error> {
 		// {{{ Control weight
 		unsafe {
 			let raw = face.raw_mut() as *mut _;
@@ -135,6 +240,7 @@ impl BitmapCanvas {
 			}
 		}
 		// }}}
+
 		face.set_char_size((style.size << 6) as isize, 0, 0, 0)?;
 
 		// {{{ Compute layout
@@ -200,81 +306,102 @@ impl BitmapCanvas {
 			y_max = 0;
 		}
 
-		// println!("{}, {} - {}, {}", x_min, y_min, x_max, y_max);
-
+		let bbox = Rect::from_extremes(x_min as i32, y_min as i32, x_max as i32, y_max as i32);
+		let pos = bbox.align(style.align, pos);
 		// }}}
+
+		Ok((pos, bbox, data))
+	}
+
+	/// Render text
+	pub fn text(
+		&mut self,
+		pos: Position,
+		face: &mut Face,
+		style: TextStyle,
+		text: &str,
+	) -> Result<(), Error> {
+		let (pos, bbox, data) = self.plan_text_rendering(pos, face, style, text)?;
+
 		// {{{ Render glyphs
 		for (pos_x, glyph) in &data {
 			let b_glyph = glyph.to_bitmap(freetype::RenderMode::Normal, None)?;
 			let bitmap = b_glyph.bitmap();
 			let pixel_mode = bitmap.pixel_mode()?;
 			assert_eq!(pixel_mode, PixelMode::Gray);
-			println!("starting to stroke");
 
-			// {{{ Blit border
-			let stroker = FREETYPE_LIB.with(|lib| lib.new_stroker())?;
-			stroker.set(1 << 6, StrokerLineCap::Round, StrokerLineJoin::Round, 0);
-			let sglyph = glyph.stroke(&stroker)?;
-			let sb_glyph = sglyph.to_bitmap(freetype::RenderMode::Normal, None)?;
-			let sbitmap = sb_glyph.bitmap();
-			let spixel_mode = sbitmap.pixel_mode()?;
-			assert_eq!(spixel_mode, PixelMode::Gray);
+			let char_pos = (
+				pos.0 + *pos_x as i32 - bbox.x,
+				pos.1 + bbox.height as i32 + bbox.y,
+			);
 
-			let iw = sbitmap.width();
-			let ih = sbitmap.rows();
-			println!("pitch {}, width {}, height {}", sbitmap.pitch(), iw, ih);
-			let height = self.buffer.len() as u32 / 3 / self.width;
-			let src = sbitmap.buffer();
-			for dx in 0..iw {
-				for dy in 0..ih {
-					let x = pos.0 + *pos_x as i32 + dx as i32 + sb_glyph.left();
-					let y = pos.1 + dy as i32 - sb_glyph.top();
-					if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
-						let gray = src[(dx + dy * iw) as usize];
-
-						let r = 255 - style.color.0;
-						let g = 255 - style.color.1;
-						let b = 255 - style.color.2;
-						let a = gray;
-
-						let color = (r, g, b, a);
-
-						self.set_pixel((x as u32, y as u32), color);
-					}
-				}
+			if let Some((shadow_color, offset)) = style.drop_shadow {
+				let char_pos = (char_pos.0 + offset.0, char_pos.1 + offset.1);
+				self.blit_glyph(&b_glyph, &bitmap, char_pos, shadow_color);
 			}
-			// }}}
-			// {{{ Blit
-			let iw = bitmap.width();
-			let ih = bitmap.rows();
-			let height = self.buffer.len() as u32 / 3 / self.width;
-			let src = bitmap.buffer();
 
-			for dx in 0..iw {
-				for dy in 0..ih {
-					let x = pos.0 + *pos_x as i32 + dx as i32 + b_glyph.left();
-					let y = pos.1 + dy as i32 - b_glyph.top();
-					if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
-						let gray = src[(dx + dy * iw) as usize];
+			if let Some((stroke_color, stroke_width)) = style.stroke {
+				// {{{ Create stroke
+				let stroker = FREETYPE_LIB.with(|lib| lib.new_stroker())?;
+				stroker.set(
+					float_to_ft_fixed(stroke_width),
+					StrokerLineCap::Round,
+					StrokerLineJoin::Round,
+					0,
+				);
 
-						let r = style.color.0;
-						let g = style.color.1;
-						let b = style.color.2;
-						let a = gray;
+				let sglyph = glyph.stroke(&stroker)?;
+				let sb_glyph = sglyph.to_bitmap(freetype::RenderMode::Normal, None)?;
+				let sbitmap = sb_glyph.bitmap();
+				let spixel_mode = sbitmap.pixel_mode()?;
+				assert_eq!(spixel_mode, PixelMode::Gray);
+				// }}}
 
-						let color = (r, g, b, a);
-
-						self.set_pixel((x as u32, y as u32), color);
-					}
-				}
+				self.blit_glyph(&sb_glyph, &sbitmap, char_pos, stroke_color);
 			}
-			// }}}
+
+			self.blit_glyph(&b_glyph, &bitmap, char_pos, style.color);
 		}
 		// }}}
 
 		Ok(())
 	}
 	// }}}
+	// {{{ Blit glyph
+	pub fn blit_glyph(
+		&mut self,
+		b_glyph: &BitmapGlyph,
+		bitmap: &Bitmap,
+		pos: Position,
+		color: Color,
+	) {
+		let iw = bitmap.width();
+		let ih = bitmap.rows();
+		let height = self.buffer.len() as u32 / 3 / self.width;
+		let src = bitmap.buffer();
+
+		for dx in 0..iw {
+			for dy in 0..ih {
+				let x = pos.0 + dx as i32 + b_glyph.left();
+				let y = pos.1 + dy as i32 - b_glyph.top();
+
+				// TODO: gamma correction
+				if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
+					let gray = src[(dx + dy * iw) as usize];
+
+					let r = color.0;
+					let g = color.1;
+					let b = color.2;
+					let a = ((color.3 as u32 * gray as u32) / 0xff) as u8;
+
+					let color = Color(r, g, b, a);
+
+					self.set_pixel((x as u32, y as u32), color);
+				}
+			}
+		}
+	}
+	// }}}
 
 	#[inline]
 	pub fn new(width: u32, height: u32) -> Self {
@@ -340,7 +467,7 @@ impl LayoutManager {
 		y: i32,
 	) {
 		let current = self.boxes[id.0];
-		let to = self.boxes[id_relative_to.0];
+
 		if let Some((current_points_to, dx, dy)) = current.relative_to
 			&& current_points_to != id_relative_to
 		{
@@ -352,7 +479,7 @@ impl LayoutManager {
 		{
 			let a = self.lookup(id);
 			let b = self.lookup(id_relative_to);
-			assert_eq!((a.0 - b.0, a.1 - b.1), (x, y));
+			assert_eq!((a.x - b.x, a.y - b.y), (x, y));
 		}
 	}
 	// }}}
@@ -414,7 +541,7 @@ impl LayoutManager {
 		&mut self,
 		id: LayoutBoxId,
 		amount: (u32, u32),
-	) -> (LayoutBoxId, impl Iterator<Item = (i32, i32)>) {
+	) -> (LayoutBoxId, impl Iterator<Item = Position>) {
 		let inner = self.boxes[id.0];
 		let outer_id = self.make_box(inner.width * amount.0, inner.height * amount.1);
 		self.edit_to_relative(id, outer_id, 0, 0);
@@ -429,13 +556,13 @@ impl LayoutManager {
 	}
 	// }}}
 	// {{{ Lookup box
-	pub fn lookup(&self, id: LayoutBoxId) -> (i32, i32, u32, u32) {
+	pub fn lookup(&self, id: LayoutBoxId) -> Rect {
 		let current = self.boxes[id.0];
 		if let Some((to, dx, dy)) = current.relative_to {
-			let (x, y, _, _) = self.lookup(to);
-			(x + dx, y + dy, current.width, current.height)
+			let r = self.lookup(to);
+			Rect::new(r.x + dx, r.y + dy, current.width, current.height)
 		} else {
-			(0, 0, current.width, current.height)
+			Rect::new(0, 0, current.width, current.height)
 		}
 	}
 
@@ -449,10 +576,17 @@ impl LayoutManager {
 		self.boxes[id.0].height
 	}
 
+	// }}}
+	// {{{ Alignment
 	#[inline]
-	pub fn position_relative_to(&self, id: LayoutBoxId, pos: (i32, i32)) -> (i32, i32) {
+	pub fn position_relative_to(&self, id: LayoutBoxId, pos: Position) -> Position {
 		let current = self.lookup(id);
-		((pos.0 as i32 + current.0), (pos.1 as i32 + current.1))
+		((pos.0 as i32 + current.x), (pos.1 as i32 + current.y))
+	}
+
+	#[inline]
+	pub fn align(&self, id: LayoutBoxId, align: (Align, Align), pos: Position) -> Position {
+		self.lookup(id).align(align, pos)
 	}
 	// }}}
 }
@@ -473,14 +607,14 @@ impl LayoutDrawer {
 	// }}}
 	// {{{ Draw RGB image
 	/// Draws a bitmap image
-	pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: (i32, i32), dims: (u32, u32), src: &[u8]) {
+	pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
 		let pos = self.layout.position_relative_to(id, pos);
 		self.canvas.blit_rbg(pos, dims, src);
 	}
 	// }}}
 	// {{{ Draw RGBA image
 	/// Draws a bitmap image taking care of the alpha channel.
-	pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: (i32, i32), dims: (u32, u32), src: &[u8]) {
+	pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
 		let pos = self.layout.position_relative_to(id, pos);
 		self.canvas.blit_rbga(pos, dims, src);
 	}
@@ -489,8 +623,11 @@ impl LayoutDrawer {
 	/// Fills with solid color
 	pub fn fill(&mut self, id: LayoutBoxId, color: Color) {
 		let current = self.layout.lookup(id);
-		self.canvas
-			.fill((current.0, current.1), (current.2, current.3), color);
+		self.canvas.fill(
+			(current.x, current.y),
+			(current.width, current.height),
+			color,
+		);
 	}
 	// }}}
 	// {{{ Draw text
@@ -498,7 +635,7 @@ impl LayoutDrawer {
 	pub fn text(
 		&mut self,
 		id: LayoutBoxId,
-		pos: (i32, i32),
+		pos: Position,
 		face: &mut Face,
 		style: TextStyle,
 		text: &str,
diff --git a/src/commands/stats.rs b/src/commands/stats.rs
index d56e11c..cc13418 100644
--- a/src/commands/stats.rs
+++ b/src/commands/stats.rs
@@ -8,7 +8,7 @@ use plotters::{
 	drawing::IntoDrawingArea,
 	element::Circle,
 	series::LineSeries,
-	style::{Color, IntoFont, TextStyle, BLUE, WHITE},
+	style::{IntoFont, TextStyle, BLUE, WHITE},
 };
 use poise::{
 	serenity_prelude::{CreateAttachment, CreateMessage},
@@ -17,8 +17,12 @@ use poise::{
 use sqlx::query_as;
 
 use crate::{
-	assets::EXO_FONT,
-	bitmap::{Align, BitmapCanvas, LayoutDrawer, LayoutManager},
+	assets::{
+		get_b30_background, get_count_background, get_difficulty_background, get_grade_background,
+		get_name_backgound, get_ptt_emblem, get_score_background, get_status_background,
+		get_top_backgound, EXO_FONT,
+	},
+	bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect},
 	chart::{Chart, Song},
 	context::{Context, Error},
 	jacket::BITMAP_IMAGE_SIZE,
@@ -214,7 +218,7 @@ pub async fn plot(
 		chart.draw_series(
 			points
 				.iter()
-				.map(|(t, s)| Circle::new((*t, *s), 3, BLUE.filled())),
+				.map(|(t, s)| Circle::new((*t, *s), 3, plotters::style::Color::filled(&BLUE))),
 		)?;
 		root.present()?;
 	}
@@ -280,16 +284,23 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
 
 	let mut layout = LayoutManager::default();
 	let jacket_area = layout.make_box(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE);
+	let jacket_with_border = layout.margin_uniform(jacket_area, 3);
 	let jacket_margin = 10;
-	let jacket_with_margin =
-		layout.margin(jacket_area, jacket_margin, jacket_margin, 5, jacket_margin);
+	let jacket_with_margin = layout.margin(
+		jacket_with_border,
+		jacket_margin,
+		jacket_margin,
+		2,
+		jacket_margin,
+	);
 	let top_left_area = layout.make_box(90, layout.height(jacket_with_margin));
 	let top_area = layout.glue_vertically(top_left_area, jacket_with_margin);
-	let bottom_area = layout.make_box(layout.width(top_area), 40);
+	let bottom_area = layout.make_box(layout.width(top_area), 43);
+	let bottom_in_area = layout.margin_xy(bottom_area, -20, -7);
 	let item_area = layout.glue_horizontally(top_area, bottom_area);
-	let item_with_margin = layout.margin_xy(item_area, 25, 20);
+	let item_with_margin = layout.margin_xy(item_area, 22, 17);
 	let (item_grid, item_origins) = layout.repeated_evenly(item_with_margin, (5, 6));
-	let root = item_grid;
+	let root = layout.margin_uniform(item_grid, 30);
 
 	// layout.normalize(root);
 	let width = layout.width(root);
@@ -298,14 +309,14 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
 	let canvas = BitmapCanvas::new(width, height);
 	let mut drawer = LayoutDrawer::new(layout, canvas);
 
-	let asset_cache = &ctx.data().jacket_cache;
-	let bg = &asset_cache.b30_background;
+	let bg = get_b30_background();
 
 	drawer.blit_rbg(
 		root,
-		(
-			-((bg.width() - width) as i32) / 2,
-			-((bg.height() - height) as i32) / 2,
+		// Align the center of the image with the center of the root
+		Rect::from_image(bg).align(
+			(Align::Center, Align::Center),
+			drawer.layout.lookup(root).center(),
 		),
 		bg.dimensions(),
 		bg.as_raw(),
@@ -316,10 +327,74 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
 			.layout
 			.edit_to_relative(item_with_margin, item_grid, origin.0, origin.1);
 
-		drawer.fill(top_area, (59, 78, 102, 255));
+		let top_bg = get_top_backgound();
+		drawer.blit_rbg(top_area, (0, 0), top_bg.dimensions(), top_bg);
 
-		let (_play, song, chart) = &plays[i];
+		let (play, song, chart) = &plays[i];
 
+		// {{{ Display index
+		let bg = get_count_background();
+		let bg_center = Rect::from_image(bg).center();
+
+		// Draw background
+		drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg);
+
+		EXO_FONT.with_borrow_mut(|font| {
+			drawer.text(
+				item_area,
+				(bg_center.0 - 12, bg_center.1 - 3 + jacket_margin),
+				font,
+				crate::bitmap::TextStyle {
+					size: 25,
+					weight: 800,
+					color: Color::WHITE,
+					align: (Align::Center, Align::Center),
+					stroke: None,
+					drop_shadow: Some((Color::BLACK.alpha(0xaa), (2, 2))),
+				},
+				&format!("#{}", i + 1),
+			)
+		})?;
+		// }}}
+		// {{{ Display chart name
+		// Draw background
+		let bg = get_name_backgound();
+		drawer.blit_rbg(bottom_area, (0, 0), bg.dimensions(), bg.as_raw());
+
+		// Draw text
+		EXO_FONT.with_borrow_mut(|font| {
+			let initial_size = 24;
+			let mut style = crate::bitmap::TextStyle {
+				size: initial_size,
+				weight: 800,
+				color: Color::WHITE,
+				align: (Align::Start, Align::Center),
+				stroke: Some((Color::BLACK, 1.5)),
+				drop_shadow: None,
+			};
+
+			while drawer
+				.canvas
+				.plan_text_rendering((0, 0), font, style, &song.title)?
+				.1
+				.width >= drawer.layout.width(bottom_in_area)
+			{
+				style.size -= 3;
+				style.stroke = Some((
+					Color::BLACK,
+					style.size as f32 / (initial_size as f32) * 1.5,
+				));
+			}
+
+			drawer.text(
+				bottom_in_area,
+				(0, drawer.layout.height(bottom_in_area) as i32 / 2),
+				font,
+				style,
+				&song.title,
+			)
+		})?;
+		// }}}
 		// {{{ Display jacket
 		let jacket = chart.cached_jacket.as_ref().ok_or_else(|| {
 			format!(
@@ -328,6 +403,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
 			)
 		})?;
 
+		drawer.fill(jacket_with_border, Color::from_rgb_int(0x271E35));
 		drawer.blit_rbg(
 			jacket_area,
 			(0, 0),
@@ -336,13 +412,15 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
 		);
 		// }}}
 		// {{{ Display difficulty background
-		let diff_bg = &asset_cache.diff_backgrounds[chart.difficulty.to_index()];
+		let diff_bg = get_difficulty_background(chart.difficulty);
+		let diff_bg_area = Rect::from_image(diff_bg).align_whole(
+			(Align::Center, Align::Center),
+			(drawer.layout.width(jacket_with_border) as i32, 0),
+		);
+
 		drawer.blit_rbga(
-			jacket_area,
-			(
-				BITMAP_IMAGE_SIZE as i32 - (diff_bg.width() as i32) / 2,
-				-(diff_bg.height() as i32) / 2,
-			),
+			jacket_with_border,
+			diff_bg_area.top_left(),
 			diff_bg.dimensions(),
 			&diff_bg.as_raw(),
 		);
@@ -356,92 +434,192 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
 			0
 		};
 
-		// EXO_FONT.with_borrow_mut(|font| {
-		// 	drawer.text(
-		// 		jacket_area,
-		// 		(BITMAP_IMAGE_SIZE as i32 + x_offset - 30, 2),
-		// 		font,
-		// 		crate::bitmap::TextStyle {
-		// 			size: 40,
-		// 			weight: 250,
-		// 			color: (0xff, 0xff, 0xff, 0xff),
-		// 			h_align: Align::Center,
-		// 			v_align: Align::Center,
-		// 		},
-		// 		&chart.level,
-		// 	)
-		// })?;
-		// {{{ Display chart name
-		// Draw background
-		drawer.fill(bottom_area, (0x82, 0x71, 0xA7, 255));
+		let diff_area_center = diff_bg_area.center();
 
-		let tx = 10;
-		let ty = drawer.layout.height(bottom_area) as i32 / 2;
-
-		// let text = &song.title;
-		// let mut size = 30;
-		// let mut text_style = TextStyle::from(("Exo", size).into_font().style(FontStyle::Bold))
-		// 	.with_anchor::<RGBAColor>(Pos {
-		// 		h_pos: HPos::Left,
-		// 		v_pos: VPos::Center,
-		// 	})
-		// 	.into_text_style(&bottom_area);
-		//
-		// while text_style.font.layout_box(text).unwrap().1 .0 >= item_area.0 as i32 - 20 {
-		// 	size -= 3;
-		// 	text_style.font = ("Exo", size).into_font();
-		// }
-		//
-		// Draw drop shadow
-		// bottom_area.draw_text(
-		// 	&song.title,
-		// 	&text_style.color(&RGBAColor(0, 0, 0, 0.2)),
-		// 	(tx + 3, ty + 3),
-		// )?;
-		// bottom_area.draw_text(
-		// 	&song.title,
-		// 	&text_style.color(&RGBAColor(0, 0, 0, 0.2)),
-		// 	(tx - 3, ty + 3),
-		// )?;
-		// bottom_area.draw_text(
-		// 	&song.title,
-		// 	&text_style.color(&RGBAColor(0, 0, 0, 0.2)),
-		// 	(tx + 3, ty - 3),
-		// )?;
-		// bottom_area.draw_text(
-		// 	&song.title,
-		// 	&text_style.color(&RGBAColor(0, 0, 0, 0.2)),
-		// 	(tx - 3, ty - 3),
-		// )?;
-
-		// Draw text
-		// bottom_area.draw_text(&song.title, &text_style.color(&WHITE), (tx, ty))?;
+		EXO_FONT.with_borrow_mut(|font| {
+			drawer.text(
+				jacket_with_border,
+				(diff_area_center.0 + x_offset, diff_area_center.1),
+				font,
+				crate::bitmap::TextStyle {
+					size: 25,
+					weight: 600,
+					color: Color::from_rgb_int(0xffffff),
+					align: (Align::Center, Align::Center),
+					stroke: None,
+					drop_shadow: None,
+				},
+				&chart.level,
+			)
+		})?;
 		// }}}
-		// {{{ Display index
-		let bg = &asset_cache.count_background;
+		// {{{ Display score background
+		let score_bg = get_score_background();
+		let score_bg_pos = Rect::from_image(score_bg).align(
+			(Align::End, Align::End),
+			(
+				drawer.layout.width(jacket_area) as i32,
+				drawer.layout.height(jacket_area) as i32,
+			),
+		);
 
-		// Draw background
-		drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg);
+		drawer.blit_rbga(
+			jacket_area,
+			score_bg_pos,
+			score_bg.dimensions(),
+			&score_bg.as_raw(),
+		);
+		// }}}
+		// {{{ Display score text
+		EXO_FONT.with_borrow_mut(|font| {
+			drawer.text(
+				jacket_area,
+				(
+					score_bg_pos.0 + 5,
+					score_bg_pos.1 + score_bg.height() as i32 / 2,
+				),
+				font,
+				crate::bitmap::TextStyle {
+					size: 23,
+					weight: 800,
+					color: Color::WHITE,
+					align: (Align::Start, Align::Center),
+					stroke: Some((Color::BLACK, 1.5)),
+					drop_shadow: None,
+				},
+				&format!("{:0>10}", format!("{}", play.score)),
+			)
+		})?;
+		// }}}
+		// {{{ Display status background
+		let status_bg = get_status_background();
+		let status_bg_area = Rect::from_image(status_bg).align_whole(
+			(Align::Center, Align::Center),
+			(
+				drawer.layout.width(jacket_area) as i32 + 3,
+				drawer.layout.height(jacket_area) as i32 + 1,
+			),
+		);
 
-		// let text_style = TextStyle::from(("Exo", 30).into_font().style(FontStyle::Bold))
-		// 	.with_anchor::<RGBAColor>(Pos {
-		// 		h_pos: HPos::Left,
-		// 		v_pos: VPos::Center,
-		// 	})
-		// 	.into_text_style(&area);
+		drawer.blit_rbga(
+			jacket_area,
+			status_bg_area.top_left(),
+			status_bg.dimensions(),
+			&status_bg.as_raw(),
+		);
+		// }}}
+		// {{{ Display status text
+		EXO_FONT.with_borrow_mut(|font| {
+			let status = play
+				.short_status(chart)
+				.ok_or_else(|| format!("Could not get status for score {}", play.score))?;
 
-		let tx = 7;
-		let ty = (jacket_margin + bg.height() as i32 / 2) - 3;
+			let x_offset = match status {
+				'P' => 2,
+				'M' => 2,
+				// TODO: ensure the F is rendered properly as well
+				_ => 0,
+			};
 
-		// Draw drop shadow
-		// area.draw_text(
-		// 	&format!("#{}", i + 1),
-		// 	&text_style.color(&BLACK),
-		// 	(tx + 2, ty + 2),
-		// )?;
+			let center = status_bg_area.center();
 
-		// Draw main text
-		// area.draw_text(&format!("#{}", i + 1), &text_style.color(&WHITE), (tx, ty))?;
+			drawer.text(
+				jacket_area,
+				(center.0 + x_offset, center.1),
+				font,
+				crate::bitmap::TextStyle {
+					size: if status == 'M' { 30 } else { 36 },
+					weight: if status == 'M' { 800 } else { 500 },
+					color: Color::WHITE,
+					align: (Align::Center, Align::Center),
+					stroke: None,
+					drop_shadow: None,
+				},
+				&format!("{}", status),
+			)
+		})?;
+		// }}}
+		// {{{ Display grade background
+		let top_left_center = (drawer.layout.width(top_left_area) as i32 + jacket_margin) / 2;
+		let grade_bg = get_grade_background();
+		let grade_bg_area = Rect::from_image(grade_bg).align_whole(
+			(Align::Center, Align::Center),
+			(top_left_center, jacket_margin + 140),
+		);
+
+		drawer.blit_rbga(
+			top_area,
+			grade_bg_area.top_left(),
+			grade_bg.dimensions(),
+			&grade_bg.as_raw(),
+		);
+		// }}}
+		// {{{ Display grade text
+		EXO_FONT.with_borrow_mut(|font| {
+			let grade = play.score.grade();
+			let center = grade_bg_area.center();
+
+			drawer.text(
+				top_left_area,
+				(center.0, center.1),
+				font,
+				crate::bitmap::TextStyle {
+					size: 30,
+					weight: 650,
+					color: Color::from_rgb_int(0x203C6B),
+					align: (Align::Center, Align::Center),
+					stroke: Some((Color::WHITE, 1.5)),
+					drop_shadow: None,
+				},
+				&format!("{}", grade),
+			)
+		})?;
+		// }}}
+		// {{{ Display rating text
+		EXO_FONT.with_borrow_mut(|font| -> Result<(), Error> {
+			let mut style = crate::bitmap::TextStyle {
+				size: 12,
+				weight: 600,
+				color: Color::WHITE,
+				align: (Align::Center, Align::Center),
+				stroke: None,
+				drop_shadow: None,
+			};
+
+			drawer.text(
+				top_left_area,
+				(top_left_center, 73),
+				font,
+				style,
+				"POTENTIAL",
+			)?;
+
+			style.size = 25;
+			style.weight = 700;
+
+			drawer.text(
+				top_left_area,
+				(top_left_center, 94),
+				font,
+				style,
+				&format!(
+					"{:.2}",
+					(play.score.play_rating(chart.chart_constant)) as f32 / 100.
+				),
+			)?;
+
+			Ok(())
+		})?;
+		// }}}
+		// {{{ Display ptt emblem
+		let ptt_emblem = get_ptt_emblem();
+		drawer.blit_rbga(
+			top_left_area,
+			Rect::from_image(ptt_emblem)
+				.align((Align::Center, Align::Center), (top_left_center, 115)),
+			ptt_emblem.dimensions(),
+			ptt_emblem.as_raw(),
+		);
 		// }}}
 	}
 
diff --git a/src/jacket.rs b/src/jacket.rs
index 8e713f9..63ac7c9 100644
--- a/src/jacket.rs
+++ b/src/jacket.rs
@@ -1,11 +1,11 @@
 use std::{fs, path::PathBuf, str::FromStr};
 
-use image::{imageops::FilterType, GenericImageView, ImageBuffer, Rgb, Rgba};
+use image::{imageops::FilterType, GenericImageView, Rgba};
 use kd_tree::{KdMap, KdPoint};
 use num::Integer;
 
 use crate::{
-	assets::should_skip_jacket_art,
+	assets::{get_assets_dir, should_skip_jacket_art},
 	chart::{Difficulty, Jacket, SongCache},
 	context::Error,
 	score::guess_chart_name,
@@ -14,7 +14,7 @@ use crate::{
 /// How many sub-segments to split each side into
 pub const SPLIT_FACTOR: u32 = 8;
 pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
-pub const BITMAP_IMAGE_SIZE: u32 = 192;
+pub const BITMAP_IMAGE_SIZE: u32 = 174;
 
 #[derive(Debug, Clone)]
 pub struct ImageVec {
@@ -77,9 +77,6 @@ impl KdPoint for ImageVec {
 
 pub struct JacketCache {
 	tree: KdMap<ImageVec, u32>,
-	pub b30_background: ImageBuffer<Rgb<u8>, Vec<u8>>,
-	pub count_background: ImageBuffer<Rgba<u8>, Vec<u8>>,
-	pub diff_backgrounds: [ImageBuffer<Rgba<u8>, Vec<u8>>; 5],
 }
 
 impl JacketCache {
@@ -87,7 +84,6 @@ impl JacketCache {
 	// This is a bit inefficient (using a hash set), but only runs once
 	pub fn new(data_dir: &PathBuf, song_cache: &mut SongCache) -> Result<Self, Error> {
 		let jacket_dir = data_dir.join("jackets");
-		let assets_dir = data_dir.join("assets");
 
 		if jacket_dir.exists() {
 			fs::remove_dir_all(&jacket_dir).expect("Could not delete jacket dir");
@@ -96,7 +92,7 @@ impl JacketCache {
 		fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir");
 
 		let tree_entries = if should_skip_jacket_art() {
-			let path = assets_dir.join("placeholder-jacket.jpg");
+			let path = get_assets_dir().join("placeholder_jacket.jpg");
 			let contents: &'static _ = fs::read(path)?.leak();
 			let image = image::load_from_memory(contents)?;
 			let bitmap: &'static _ = Box::leak(Box::new(
@@ -210,21 +206,6 @@ impl JacketCache {
 
 		let result = Self {
 			tree: KdMap::build_by_ordered_float(tree_entries),
-			b30_background: image::open(assets_dir.join("b30_background.jpg"))?
-				.resize(2048 * 2, 1535 * 2, FilterType::Nearest)
-				.blur(20.0)
-				.into_rgb8(),
-			count_background: image::open(assets_dir.join("count_background.png"))?
-				.blur(1.0)
-				.into_rgba8(),
-			diff_backgrounds: Difficulty::DIFFICULTY_SHORTHANDS.try_map(
-				|shorthand| -> Result<_, Error> {
-					Ok(image::open(
-						assets_dir.join(format!("diff-{}.png", shorthand.to_lowercase())),
-					)?
-					.into_rgba8())
-				},
-			)?,
 		};
 
 		Ok(result)
diff --git a/src/main.rs b/src/main.rs
index 3a29ab4..b959709 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,7 +13,7 @@ mod jacket;
 mod score;
 mod user;
 
-use assets::DATA_DIR;
+use assets::get_data_dir;
 use context::{Error, UserContext};
 use poise::serenity_prelude::{self as serenity};
 use sqlx::sqlite::SqlitePoolOptions;
@@ -33,7 +33,7 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
 
 #[tokio::main]
 async fn main() {
-	let data_dir = DATA_DIR.with(|d| d.clone());
+	let data_dir = get_data_dir();
 	let cache_dir = var("SHIMMERING_CACHE_DIR").expect("Missing `SHIMMERING_CACHE_DIR` env var");
 
 	let pool = SqlitePoolOptions::new()
diff --git a/src/score.rs b/src/score.rs
index 11bc09e..2855793 100644
--- a/src/score.rs
+++ b/src/score.rs
@@ -18,6 +18,34 @@ use crate::context::{Error, UserContext};
 use crate::jacket::IMAGE_VEC_DIM;
 use crate::user::User;
 
+// {{{ Grade
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Grade {
+	EXP,
+	EX,
+	AA,
+	A,
+	B,
+	C,
+	D,
+}
+
+impl Grade {
+	pub const GRADE_STRINGS: [&'static str; 7] = ["EX+", "EX", "AA", "A", "B", "C", "D"];
+	pub const GRADE_SHORTHANDS: [&'static str; 7] = ["exp", "ex", "aa", "a", "b", "c", "d"];
+
+	#[inline]
+	pub fn to_index(self) -> usize {
+		self as usize
+	}
+}
+
+impl Display for Grade {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		write!(f, "{}", Self::GRADE_STRINGS[self.to_index()])
+	}
+}
+// }}}
 // {{{ Score
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
 pub struct Score(pub u32);
@@ -110,22 +138,22 @@ impl Score {
 	// {{{ Score => grade
 	#[inline]
 	// TODO: Perhaps make an enum for this
-	pub fn grade(self) -> &'static str {
+	pub fn grade(self) -> Grade {
 		let score = self.0;
 		if score > 9900000 {
-			"EX+"
+			Grade::EXP
 		} else if score > 9800000 {
-			"EX"
+			Grade::EX
 		} else if score > 9500000 {
-			"AA"
+			Grade::AA
 		} else if score > 9200000 {
-			"A"
+			Grade::A
 		} else if score > 8900000 {
-			"B"
+			Grade::B
 		} else if score > 8600000 {
-			"C"
+			Grade::C
 		} else {
-			"D"
+			Grade::D
 		}
 	}
 	// }}}
@@ -477,7 +505,6 @@ impl Play {
 	pub fn status(&self, chart: &Chart) -> Option<String> {
 		let score = self.score.0;
 		if score >= 10_000_000 {
-			// Prevent subtracting with overflow
 			if score > chart.note_count + 10_000_000 {
 				return None;
 			}
@@ -502,6 +529,25 @@ impl Play {
 			None
 		}
 	}
+
+	#[inline]
+	pub fn short_status(&self, chart: &Chart) -> Option<char> {
+		let score = self.score.0;
+		if score >= 10_000_000 {
+			let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?;
+			if non_max_pures == 0 {
+				Some('M')
+			} else {
+				Some('P')
+			}
+		} else if let Some(distribution) = self.distribution(chart.note_count)
+			&& distribution.3 == 0
+		{
+			Some('F')
+		} else {
+			Some('C')
+		}
+	}
 	// }}}
 	// {{{ Play to embed
 	/// Creates a discord embed for this play.
@@ -534,7 +580,7 @@ impl Play {
 				),
 				true,
 			)
-			.field("Grade", self.score.grade(), true)
+			.field("Grade", format!("{}", self.score.grade()), true)
 			.field("ξ-Score", format!("{} (+?)", self.zeta_score), true)
 			.field(
 				"ξ-Rating",
@@ -544,7 +590,7 @@ impl Play {
 				),
 				true,
 			)
-			.field("ξ-Grade", self.zeta_score.grade(), true)
+			.field("ξ-Grade", format!("{}", self.zeta_score.grade()), true)
 			.field(
 				"Status",
 				self.status(chart).unwrap_or("?".to_string()),