---
title: "Page Turn"
description: "A small book of Tang poems whose pages turn around the spine, with the art on the back of each leaf."
canonical_url: "https://rn-carousel.dev/Examples/scenes/page-turn"
markdown_url: "https://rn-carousel.dev/Examples/scenes/page-turn.md"
version: "5"
---
# Page Turn

A small book of Tang poems whose pages turn around the spine, with the art on the back of each leaf.

### How it works

1. `itemSize` is one page wide and `itemAnimation` hangs all 8 leaves on the spine, with the leaf in the air (value between −1 and 0) on top at `zIndex` 300.
2. Inside each leaf, `relativeProgress` turns the front from 0° to −180° with `perspective` 1400 around its left edge, and the back follows 180° behind with `backfaceVisibility` hidden.
3. With `loop` false, `autoplay` turns a page every 2400 ms; when `onSnapToItem` reports the last page, `ref.scrollTo({ index: 0 })` closes the book and autoplay starts again.

### Uses

- [`itemAnimation`](https://rn-carousel.dev/custom-animations.md)
- [`itemSize`](https://rn-carousel.dev/props.md)
- [`relativeProgress`](https://rn-carousel.dev/custom-animations.md)
- [`loop`](https://rn-carousel.dev/props.md)
- [`autoplay`](https://rn-carousel.dev/props.md)
- [`autoplayInterval`](https://rn-carousel.dev/props.md)
- [`progress`](https://rn-carousel.dev/props.md)
- [`onSnapToItem`](https://rn-carousel.dev/props.md)
- [`ref.prev()`](https://rn-carousel.dev/programmatic-control.md)
- [`ref.next()`](https://rn-carousel.dev/programmatic-control.md)
- [`ref.scrollTo()`](https://rn-carousel.dev/programmatic-control.md)

## Full source

```tsx copy
import { SceneScreen, useScene } from "@/features/scenes/kit";
import { LinearGradient } from "expo-linear-gradient";
import * as React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
	Easing,
	Extrapolation,
	type SharedValue,
	interpolate,
	useAnimatedReaction,
	useAnimatedStyle,
	useDerivedValue,
	useSharedValue,
} from "react-native-reanimated";
import {
	Carousel,
	type CarouselItemAnimation,
	type CarouselRef,
	type CarouselRenderItemInfo,
} from "react-native-reanimated-carousel";
import Svg, { Defs, Path, RadialGradient, Rect, Stop } from "react-native-svg";
import { scheduleOnRN } from "react-native-worklets";

// English renderings of six Tang dynasty poems (the originals are in the public domain).
// Each poem: "title · author · art", then one verse per line; "|" wraps a verse with a hanging indent.
const TEXT = `Quiet Night Thoughts · Li Bai · moon
Moonlight before my bed,
like frost upon the floor.
I raise my head to|the bright moon,
lower it, and|think of home.

Spring Dawn · Meng Haoran · blossom
Spring sleep, I|miss the dawn;
everywhere the|birds are singing.
Last night, the sound|of wind and rain.
How many petals fell?

At Heron Tower · Wang Zhihuan · river
The white sun sinks|behind the hills,
the Yellow River|runs to the sea.
To see a thousand|miles further,
climb one more floor.

Longing · Wang Wei · beans
Red beans grow in|the southern land;
how many sprigs|this spring?
Gather plenty of|them for me.
Nothing holds|more longing.

River Snow · Liu Zongyuan · snow
A thousand hills,|no birds in flight;
ten thousand paths,|no footprints.
A lone boat, an old|man in a straw cape,
fishing alone in the|cold river snow.

Deer Park · Wang Wei · forest
Empty hills, no|one in sight,
only the sound of voices.
Evening light enters|the deep wood
and falls again on|the green moss.`;

type Art = "moon" | "blossom" | "river" | "beans" | "snow" | "forest";
const POEMS = TEXT.split("\n\n").map((block) => {
	const [head, ...verses] = block.split("\n");
	const [title, author, art] = head.split(" · ");
	return {
		title,
		author,
		art: art as Art,
		verses: verses.map((verse) => verse.split("|")),
	};
});

// Leaf 0 is the cover, leaves 1–6 carry the poems and leaf 7 is the back endpaper.
// The back of each leaf faces the next poem: its ink drawing, or "The End" after the last one.
const LEAVES = Array.from({ length: POEMS.length + 2 }, (_, i) => i);
const LAST = LEAVES.length - 1;
const INTERVAL = 2400;
const EASE_OUT_QUART = Easing.bezier(0.25, 1, 0.5, 1);
const PAGE = { width: 190, height: 440 }; // the page is laid out at this size, then scaled to fit

const PAPER = "#FBF6EA";
const DARK = "#2E2A26";
const NAVY = "#1F2A44";
const CREAM = "#F5ECD7";
const GOLD = "#E8D5A9";
const LIFT = "0 30px 50px -20px rgba(0,0,0,0.7)";
const GUTTER = [
	"transparent",
	"#0000002E",
	"#00000040",
	"#0000002E",
	"transparent",
] as const;

// ---------- ink drawings ----------
function rng(seed: number) {
	let s = seed;
	return () => {
		s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
		return s / 4294967296;
	};
}
// A soft hill line across the page, closed along the bottom edge.
function ridge(seed: number, base: number, amp: number) {
	const r = rng(seed);
	const p = Array.from({ length: 7 }, (_, k) => [
		-10 + 35 * k,
		base - r() * amp,
	]);
	const mid = (k: number) =>
		`${(p[k][0] + p[k + 1][0]) / 2} ${(p[k][1] + p[k + 1][1]) / 2}`;
	let d = `M-10 440L${p[0][0]} ${p[0][1]}L${mid(0)}`;
	for (let k = 1; k < 6; k++) d += `Q${p[k][0]} ${p[k][1]} ${mid(k)}`;
	return `${d}L${p[6][0]} ${p[6][1]}L200 440Z`;
}
// An ellipse as two arcs, turned by `deg` around its center.
function ellipse(x: number, y: number, rx: number, ry: number, deg = 0) {
	const dx = ry * Math.sin((deg * Math.PI) / 180);
	const dy = ry * Math.cos((deg * Math.PI) / 180);
	const arc = `A${rx} ${ry} ${deg} 1 0`;
	return `M${x + dx} ${y - dy}${arc} ${x - dx} ${y + dy}${arc} ${x + dx} ${y - dy}Z`;
}
const repeat = (n: number, part: (k: number) => string) =>
	Array.from({ length: n }, (_, k) => part(k)).join("");
const BLOSSOMS = [
	60, 132, 96, 140, 132, 116, 162, 98, 118, 186, 140, 206, 44, 150,
];
const BEANS = [60, 112, 76, 120, 92, 108, 128, 126, 144, 138, 112, 98];
const flake = rng(40);
const tree = (k: number) =>
	`M${18 + k * 20} 320l10-${60 + (k % 3) * 18}l10 ${60 + (k % 3) * 18}Z`;
const INK = "#3B3530";

// Layers in the 190 x 440 page: [path, fill, opacity, stroke, strokeWidth].
const ART: Record<Art, [string, string, number, string?, number?][]> = {
	moon: [
		[ellipse(120, 120, 34, 34), "#F3E6C4", 1, "#C9B48A", 1],
		["M34 70h122v150H34Z", "none", 0.75, INK, 3],
		["M95 70v150M34 145h122", "none", 0.6, INK, 2],
		[ridge(2, 340, 50), INK, 0.18],
	],
	blossom: [
		["M10 150C70 120 110 160 180 90", "none", 0.8, INK, 5],
		["M80 138C90 170 120 180 140 210", "none", 0.7, INK, 3],
		[
			repeat(7, (k) => ellipse(BLOSSOMS[2 * k], BLOSSOMS[2 * k + 1], 7, 7)),
			"#E8A0A8",
			1,
		],
		[
			repeat(14, (k) => `M${14 + k * 13} ${250 + (k % 3) * 10}l-6 22`),
			"none",
			0.5,
			"#7A8B99",
			1.4,
		],
	],
	river: [
		[ellipse(130, 140, 26, 26), "#C8553D", 0.85],
		[ridge(5, 200, 60), INK, 0.55],
		[
			"M0 330C60 300 120 350 190 320L190 360C120 380 60 340 0 370Z",
			"#B9A27A",
			0.55,
		],
		["M40 260h16v-30h-6v-10h-4v10h-6Z", INK, 0.8],
	],
	beans: [
		["M20 90C70 100 120 70 170 120", "none", 1, INK, 3],
		[
			repeat(6, (k) => ellipse(BEANS[2 * k], BEANS[2 * k + 1], 7, 9, k * 20)),
			"#B3202C",
			1,
		],
		[ridge(8, 360, 40), INK, 0.12],
	],
	snow: [
		[ridge(12, 220, 70), "#9AA6AE", 0.45],
		[ridge(13, 270, 50), "#5B646B", 0.35],
		["M0 340H190", "none", 0.3, INK, 1],
		["M70 330q25 10 50 0l-6 8h-38Z", INK, 1],
		["M96 330v-26M92 314l4-10 6 10Z", INK, 1, INK, 1.5],
		["M96 306L140 350", "none", 1, INK, 1],
		[
			repeat(40, () => ellipse(flake() * 190, flake() * 300, 1.6, 1.6)),
			"#7A8B99",
			0.5,
		],
	],
	forest: [
		[ridge(21, 180, 60), "#5B646B", 0.25],
		[repeat(5, (k) => tree(2 * k)), "#2F3B33", 0.45],
		[repeat(4, (k) => tree(2 * k + 1)), "#2F3B33", 0.65],
		["M120 40L60 300M140 40L110 300", "none", 0.35, "#E8D9A8", 10],
	],
};

// ---------- page faces, drawn at 190 x 440 ----------
const GRAIN = repeat(110, (k) => `M0 ${k * 4}h190v2H0z`);
const stripes = (w: number) =>
	repeat(60, (k) => `M${4 * k * w - 440} 0l440 440h${w}l-440-440z`);

// Warm paper with a soft highlight and faint ruled grain.
function Paper({ children }: { children?: React.ReactNode }) {
	return (
		<Svg width={190} height={440} style={StyleSheet.absoluteFill}>
			<Defs>
				<RadialGradient
					id="light"
					cx={57}
					cy={88}
					r={226}
					gradientUnits="userSpaceOnUse"
				>
					<Stop offset="0" stopColor="#FFFFFF" stopOpacity={0.5} />
					<Stop offset="1" stopColor="#FFFFFF" stopOpacity={0} />
				</RadialGradient>
			</Defs>
			<Rect width={190} height={440} fill={PAPER} />
			<Rect width={190} height={440} fill="url(#light)" />
			<Path d={GRAIN} fill="#786446" fillOpacity={0.03} />
			{children}
		</Svg>
	);
}

function Endpaper({
	side,
	children,
}: {
	side: "left" | "right";
	children?: React.ReactNode;
}) {
	return (
		<View style={[styles.face, styles[side], styles.endpaper]}>
			<Svg width={190} height={440} style={StyleSheet.absoluteFill}>
				<Path d={stripes(6)} fill="#742B2B" />
			</Svg>
			{children}
		</View>
	);
}

function Cover() {
	return (
		<View style={[styles.face, styles.right, styles.cover]}>
			<Svg width={190} height={440} style={StyleSheet.absoluteFill}>
				<Path
					d={stripes(4)}
					fill="#24304D"
					transform="matrix(-1 0 0 1 190 0)"
				/>
				<Path
					d="M15 0V440"
					stroke={GOLD}
					strokeOpacity={0.55}
					strokeWidth={2}
					strokeDasharray="6 4"
				/>
			</Svg>
			<View style={[styles.label, styles.labelPad]}>
				<View style={styles.labelFrame} />
				<Text style={styles.labelTitle}>Six Tang Poems</Text>
				<Text style={styles.labelSub}>{"Li Bai · Wang Wei\nand others"}</Text>
			</View>
		</View>
	);
}

function Front({ leaf }: { leaf: number }) {
	if (leaf === 0) return <Cover />;
	if (leaf === LAST) return <Endpaper side="right" />;
	const poem = POEMS[leaf - 1];
	return (
		<View style={[styles.face, styles.right]}>
			<Paper />
			<View style={styles.poem}>
				<Text style={styles.poemTitle}>{poem.title}</Text>
				<Text style={styles.author}>{poem.author.toUpperCase()}</Text>
				<View style={styles.verses}>
					{poem.verses.map((parts) => (
						<View key={parts[0]} style={styles.verse}>
							{parts.map((part, k) => (
								<Text key={part} style={[styles.line, k > 0 && styles.hang]}>
									{part}
								</Text>
							))}
						</View>
					))}
				</View>
			</View>
			<View style={[styles.seal, styles.sealInk]}>
				<View style={styles.sealRing} />
			</View>
			<Text style={styles.pageNo}>{leaf}</Text>
		</View>
	);
}

function Back({ leaf }: { leaf: number }) {
	if (leaf === LAST) return null;
	const art = leaf < POEMS.length ? ART[POEMS[leaf].art] : [];
	return (
		<View style={[styles.face, styles.left]}>
			<Paper>
				{art.map(([d, fill, opacity, stroke, strokeWidth]) => (
					<Path
						key={d}
						d={d}
						fill={fill}
						opacity={opacity}
						stroke={stroke}
						strokeWidth={strokeWidth}
						strokeLinecap="round"
					/>
				))}
			</Paper>
			{leaf === POEMS.length && (
				<>
					<Text style={styles.end}>The End</Text>
					<Text style={[styles.foot, styles.colophon]}>
						Poems from the Tang dynasty (618–907), in the public domain. English
						renderings made for this demo.
					</Text>
				</>
			)}
		</View>
	);
}

// ---------- a leaf turning on the spine ----------
// A page in the air darkens toward the spine.
function Shade({ style, spineRight }: { style: object; spineRight?: boolean }) {
	return (
		<Animated.View style={[styles.face, style]}>
			<LinearGradient
				colors={["rgba(0,0,0,0.55)", "rgba(0,0,0,0.1)"]}
				start={{ x: spineRight ? 1 : 0, y: 0 }}
				end={{ x: spineRight ? 0 : 1, y: 0 }}
				style={styles.fill}
			/>
		</Animated.View>
	);
}

interface LeafProps {
	leaf: number;
	page: { width: number; height: number; top: number };
	relativeProgress: SharedValue<number>;
}

// The front turns from 0° to −180° around the spine; the back follows 180° behind it.
const Leaf = React.memo(function Leaf({
	leaf,
	page,
	relativeProgress,
}: LeafProps) {
	const scale = page.width / PAGE.width;
	const angle = useDerivedValue(() =>
		interpolate(
			relativeProgress.value,
			[-1, 0],
			[-180, 0],
			Extrapolation.CLAMP,
		),
	);
	const front = useAnimatedStyle(() => ({
		transform: [
			{ perspective: 1400 * scale },
			{ rotateY: `${angle.value}deg` },
		],
	}));
	const back = useAnimatedStyle(() => ({
		transform: [
			{ perspective: 1400 * scale },
			{ rotateY: `${angle.value + 180}deg` },
		],
	}));
	const shade = useAnimatedStyle(() => ({
		opacity: interpolate(
			relativeProgress.value,
			[-1, -0.5, 0],
			[0, 0.45, 0],
			Extrapolation.CLAMP,
		),
	}));
	const fit = [styles.sheet, { transform: [{ scale }] }];
	return (
		<>
			<Animated.View style={[styles.side, styles.frontSide, page, front]}>
				<View style={fit}>
					<Front leaf={leaf} />
					<Shade style={shade} />
				</View>
			</Animated.View>
			<Animated.View
				style={[
					styles.side,
					styles.backSide,
					page,
					{ left: -page.width },
					back,
				]}
			>
				<View style={fit}>
					<Back leaf={leaf} />
					<Shade style={shade} spineRight />
				</View>
			</Animated.View>
		</>
	);
});

function Chevron({ flip }: { flip?: boolean }) {
	return (
		<Svg
			width={20}
			height={20}
			viewBox="0 0 24 24"
			style={flip && { transform: [{ scaleX: -1 }] }}
		>
			<Path
				d="M15 5l-7 7 7 7"
				stroke={CREAM}
				strokeWidth={2}
				fill="none"
				strokeLinecap="round"
				strokeLinejoin="round"
			/>
		</Svg>
	);
}

function PageTurn() {
	const { width, height, insets } = useScene();
	const ref = React.useRef<CarouselRef>(null);
	const progress = useSharedValue(0);
	const [settled, setSettled] = React.useState(0); // the page the book came to rest on
	const [shown, setShown] = React.useState(0); // the nearest page, for the caption

	useAnimatedReaction(
		() => Math.min(LAST, Math.max(0, Math.round(progress.value))),
		(current, previous) => {
			if (current !== previous) scheduleOnRN(setShown, current);
		},
	);

	// Two pages meet at the spine in the middle of the screen.
	const room = height - insets.top - Math.max(insets.bottom, 12) - 280;
	const pageWidth = Math.floor(
		Math.min((width - 26) / 2, (room * PAGE.width) / PAGE.height),
	);
	const pageHeight = Math.round((pageWidth * PAGE.height) / PAGE.width);
	const lift = Math.round(pageHeight * 0.1); // headroom for a page that rises toward you as it turns
	const spine = width / 2;
	const page = { width: pageWidth, height: pageHeight, top: lift };
	const stage = { width, height: pageHeight + 2 * lift };

	const itemAnimation = React.useCallback<CarouselItemAnimation>(
		(value) => {
			"worklet";
			// Every leaf hangs on the spine and the one in the air stays on top.
			// Only the two facing pages and the turning leaf are drawn.
			const turning = value > -1 && value < 0;
			return {
				transform: [{ translateX: spine }],
				zIndex: turning
					? 300
					: value <= -1
						? 200 + Math.round(value)
						: 100 - Math.round(value),
				opacity: value > -2 && value < 1 ? 1 : 0,
			};
		},
		[spine],
	);

	// After the last page, close the book and start again.
	React.useEffect(() => {
		if (settled !== LAST) return;
		const timer = setTimeout(
			() => ref.current?.scrollTo({ index: 0 }),
			INTERVAL,
		);
		return () => clearTimeout(timer);
	}, [settled]);

	// Tap the right page to turn forward, the left page to turn back.
	const tap = React.useMemo(
		() =>
			Gesture.Tap()
				.runOnJS(true)
				.onEnd((e, success) => {
					if (success) e.x > spine ? ref.current?.next() : ref.current?.prev();
				}),
		[spine],
	);

	const renderItem = React.useCallback(
		({ item, relativeProgress }: CarouselRenderItemInfo<number>) => (
			<Leaf leaf={item} page={page} relativeProgress={relativeProgress} />
		),
		[pageWidth, lift],
	);

	const fill = useAnimatedStyle(() => ({
		transform: [{ scaleX: Math.max(0, Math.min(1, progress.value / LAST)) }],
	}));
	const poem = POEMS[shown - 1];
	const caption = poem?.title ?? (shown === 0 ? "Six Tang Poems" : "The End");
	const detail = poem
		? `${poem.author} · page ${shown} of ${POEMS.length}`
		: shown === 0
			? "Cover"
			: "Colophon";

	return (
		<View style={styles.fill}>
			<Svg width={width} height={height} style={StyleSheet.absoluteFill}>
				<Rect width={width} height={height} fill="#4A372B" />
				<Path
					d={repeat(
						Math.ceil(width / 7),
						(k) => `M${k * 7 + 3} 0h4v${height}h-4z`,
					)}
					fill="#503C2F"
				/>
			</Svg>
			<LinearGradient
				colors={["rgba(0,0,0,0.35)", "transparent"]}
				style={[styles.topShade, { height: insets.top + 80 }]}
			/>

			<View style={[styles.topBar, { marginTop: insets.top }]}>
				<View style={styles.barSide}>
					<Chevron />
					<Text style={styles.barText}>Library</Text>
				</View>
				<Text style={styles.bookTitle}>Six Tang Poems</Text>
				<Text style={[styles.barSide, styles.aa]}>Aa</Text>
			</View>

			<View style={styles.desk}>
				<View style={stage}>
					<View
						style={[
							styles.board,
							{
								left: spine - pageWidth - 5,
								top: lift - 6,
								width: 2 * pageWidth + 10,
								height: pageHeight + 12,
							},
						]}
					/>
					<View style={[styles.side, page, { left: spine - pageWidth }]}>
						<View
							style={[
								styles.sheet,
								{ transform: [{ scale: pageWidth / PAGE.width }] },
							]}
						>
							<Endpaper side="left">
								<Text style={[styles.exLibrisBox, styles.exLibris]}>
									EX LIBRIS
								</Text>
							</Endpaper>
						</View>
					</View>
					<GestureDetector gesture={tap}>
						<View collapsable={false} style={StyleSheet.absoluteFill}>
							<Carousel
								ref={ref}
								data={LEAVES}
								loop={false}
								autoplay={settled !== LAST}
								autoplayInterval={INTERVAL}
								itemSize={pageWidth}
								style={stage}
								animation={{
									type: "timing",
									duration: 900,
									easing: EASE_OUT_QUART,
								}}
								progress={progress}
								itemAnimation={itemAnimation}
								onSnapToItem={setSettled}
								renderItem={renderItem}
							/>
						</View>
					</GestureDetector>
					<LinearGradient
						colors={GUTTER}
						locations={[0, 0.45, 0.5, 0.55, 1]}
						start={{ x: 0, y: 0 }}
						end={{ x: 1, y: 0 }}
						style={[
							styles.gutter,
							{ left: spine - 8, top: lift, height: pageHeight },
						]}
					/>
				</View>
			</View>

			<View
				style={[
					styles.bottom,
					{ paddingBottom: Math.max(insets.bottom, 12) + 8 },
				]}
			>
				<View style={styles.track}>
					<Animated.View style={[styles.trackFill, fill]} />
				</View>
				<View style={styles.nav}>
					<Pressable
						accessibilityLabel="Previous page"
						onPress={() => ref.current?.prev()}
						style={[styles.navButton, styles.center]}
					>
						<Chevron />
					</Pressable>
					<View style={styles.captionBox}>
						<Text style={styles.caption} numberOfLines={1}>
							{caption}
						</Text>
						<Text style={styles.detail}>{detail.toUpperCase()}</Text>
					</View>
					<Pressable
						accessibilityLabel="Next page"
						onPress={() => ref.current?.next()}
						style={[styles.navButton, styles.center]}
					>
						<Chevron flip />
					</Pressable>
				</View>
			</View>
		</View>
	);
}

export default function Scene() {
	return (
		<SceneScreen
			background="#4A372B"
			statusBar="light"
			fonts={[
				"Garamond-Italic",
				"Garamond-SemiBold",
				"Fraunces-SemiBold",
				"Instrument-Regular",
				"Instrument-Medium",
				"JetBrainsMono-Regular",
				"JetBrainsMono-Medium",
			]}
		>
			<PageTurn />
		</SceneScreen>
	);
}

const font = (
	fontFamily: string,
	fontSize: number,
	lineHeight: number,
	color: string,
) => ({
	fontFamily,
	fontSize,
	lineHeight,
	color,
});
const styles = StyleSheet.create({
	fill: { flex: 1 },
	topShade: { position: "absolute", left: 0, right: 0, top: 0 },
	topBar: {
		height: 52,
		flexDirection: "row",
		alignItems: "center",
		paddingHorizontal: 16,
	},
	barSide: { width: 88, flexDirection: "row", alignItems: "center", gap: 2 },
	barText: font("Instrument-Medium", 15, 20, CREAM),
	bookTitle: {
		...font("Fraunces-SemiBold", 17, 22, CREAM),
		flex: 1,
		textAlign: "center",
	},
	aa: { ...font("Garamond-SemiBold", 19, 24, CREAM), textAlign: "right" },
	desk: { flex: 1, justifyContent: "center" },
	board: {
		position: "absolute",
		borderRadius: 10,
		backgroundColor: "#5E1F1F",
		boxShadow: LIFT,
	},
	gutter: { position: "absolute", width: 16, pointerEvents: "none" },
	// A page on screen holds the 190 x 440 sheet, scaled to fill it.
	side: {
		position: "absolute",
		alignItems: "center",
		justifyContent: "center",
	},
	frontSide: {
		left: 0,
		transformOrigin: "left center",
		backfaceVisibility: "hidden",
	},
	backSide: { transformOrigin: "right center", backfaceVisibility: "hidden" },
	sheet: { width: PAGE.width, height: PAGE.height },
	// Everything below is in sheet units, the same numbers as the prototype's CSS.
	face: { ...StyleSheet.absoluteFillObject, overflow: "hidden" },
	right: { borderTopRightRadius: 6, borderBottomRightRadius: 6 },
	left: { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
	endpaper: {
		backgroundColor: "#6B2626",
		alignItems: "center",
		justifyContent: "center",
	},
	exLibris: { ...font("Garamond-SemiBold", 13, 15, GOLD), letterSpacing: 3.9 },
	exLibrisBox: {
		paddingVertical: 10,
		paddingHorizontal: 14,
		borderWidth: 2,
		borderColor: GOLD,
	},
	cover: { backgroundColor: "#1F2A44" },
	label: {
		position: "absolute",
		left: 40,
		right: 22,
		top: 40,
		backgroundColor: CREAM,
	},
	labelPad: { paddingTop: 20, paddingBottom: 16, paddingHorizontal: 12 },
	labelFrame: {
		...StyleSheet.absoluteFillObject,
		margin: 3,
		borderWidth: 1,
		borderColor: NAVY,
	},
	labelTitle: {
		...font("Garamond-SemiBold", 24, 26, NAVY),
		textAlign: "center",
	},
	labelSub: {
		...font("Garamond-Italic", 11.5, 15, "#4B5675"),
		textAlign: "center",
		marginTop: 10,
	},
	poem: { position: "absolute", left: 20, right: 14, top: 40 },
	poemTitle: font("Garamond-SemiBold", 21, 23, DARK),
	author: {
		...font("JetBrainsMono-Medium", 9.5, 12, "#8A7A66"),
		letterSpacing: 1.1,
		marginTop: 8,
	},
	verses: { marginTop: 22 },
	verse: { marginBottom: 7 },
	line: font("Garamond-Italic", 15.5, 20, DARK),
	hang: { marginLeft: 14 },
	seal: {
		position: "absolute",
		left: 22,
		bottom: 48,
		width: 22,
		height: 22,
		padding: 2.5,
	},
	sealInk: {
		borderRadius: 2,
		backgroundColor: "#B3202C",
		opacity: 0.85,
		transform: [{ rotate: "-4deg" }],
	},
	sealRing: { flex: 1, borderWidth: 1.5, borderColor: PAPER },
	pageNo: {
		...font("JetBrainsMono-Regular", 11, 14, "#A0907A"),
		position: "absolute",
		right: 18,
		bottom: 14,
	},
	end: {
		...font("Garamond-Italic", 36, 40, "#8A7A66"),
		textAlign: "center",
		marginTop: 200,
	},
	colophon: {
		...font("Instrument-Regular", 11, 16, "#A0907A"),
		textAlign: "center",
	},
	foot: { position: "absolute", left: 20, right: 20, bottom: 30 },
	bottom: { paddingHorizontal: 20, gap: 14 },
	track: {
		height: 3,
		borderRadius: 2,
		overflow: "hidden",
		backgroundColor: `${CREAM}2E`,
	},
	trackFill: { flex: 1, backgroundColor: GOLD, transformOrigin: "left center" },
	nav: { flexDirection: "row", alignItems: "center", gap: 12 },
	navButton: {
		width: 44,
		height: 44,
		borderRadius: 22,
		backgroundColor: `${CREAM}1A`,
	},
	center: { alignItems: "center", justifyContent: "center" },
	captionBox: { flex: 1, alignItems: "center", gap: 2 },
	caption: font("Garamond-Italic", 20, 26, CREAM),
	detail: {
		...font("JetBrainsMono-Regular", 11, 16, "#C9B48A"),
		letterSpacing: 0.6,
	},
});

```
