Mathematics is the music of reason, James Joseph Sylvester
If you’re going through hell, keep on going, Winston Churchill.

React hooks let you add state and side effects to function components while keeping the rendering logic pure.
This document explains hooks by comparing them to a traditional Python UI (PySide6) class that mixes data and display.
In a typical PySide6 widget, the same class holds the data, the business logic, and the rendering calls:
class MyPrayerTab(QWidget):
def __init__(self):
self._db = PrayerDB()
self._current = None # ← data
self._page_idx = 0 # ← data
self._build_ui() # ← UI
def _show_today(self):
self._current = self._db.today_prayer() # ← fetch
self._render_page() # ← render
Every time data changes, you must explicitly call a render method. When the app grows, this mix of responsibilities makes testing and reusing parts difficult.
React enforces a clean split. React separates data logic from rendering.
┌─────────────────────────────────────────────────────────────────┐
│ usePrayers (hook) │ PrayersScreen (component) │
│ │ │
│ Owns: │ Receives from hook: │
│ prayer state │ prayer, loading, error │
│ page index │ currentPage, totalPages │
│ loading / error state │ goNext, goPrev, refresh │
│ database queries │ │
│ │ Only does: │
│ │ render(data), return UI data... │
└─────────────────────────────────────────────────────────────────┘
This separation makes both the logic and the UI easier to test, maintain, and reuse.
In Python you mutate a property and then manually tell the UI to update:
PYTHON (observer) REACT (hooks)
────────────────────────────────── ────────────────────────────────────
self._current = prayer const [prayer, setPrayer] = useState(null)
# UI reads self._current // UI reads prayer directly
self._current = new_prayer setPrayer(new_prayer)
self._render_page() // React re-renders automatically
# Manual "notify observers" // No manual notification needed
# to update dependent widgets // useState triggers a re-render
The key insight is this: setState(value) in React is equivalent to self.field = value; self.notify_observers() in Python. In other words, the UI automatically re-renders whenever state changes.
All hooks must be called at the top level of a component or custom hook (never inside loops or conditions).
useState<T>(initialValue)Adds local state to a function component.
const [prayer, setPrayer] = useState<Prayer | null>(null);
// It creates a React state variable named prayer and a function (setPrayer) to update it.
// prayer: Holds the current data. It starts as null, but later it can hold an object of type Prayer.
// setPrayer: The function used to change prayer. Calling this function will update the data and re-render your component
setPrayer(somePrayer); // triggers a re-render with the new value
Python equivalent:
self._prayer = None # useState(null)
self._prayer = some_prayer # setPrayer(some_prayer)
self.render() # ← React does this automatically
useEffect(fn, deps)Runs side effects (data fetching, subscriptions, manual DOM changes) after render.
useEffect(() => {
loadTodayPrayer();
}, [db]);
// deps = [db]: run loadTodayPrayer() when `db` changes (and on mount)
// deps = []: run once on mount, never again
// deps absent: run on every render (usually wrong — rarely intended)
Python equivalent:
def __init__(self): # useEffect with [] — runs once
self._load_today_prayer()
# For dependency tracking there is no direct equivalent.
useCallback(fn, deps)Returns a memoized version of the callback that only changes if one of the dependencies has changed.
This is essential to prevent infinite loops when a callback is used inside useEffect or passed to child components.
const refresh = useCallback(async () => {
await loadTodayPrayer();
}, [db]);
// In React, const refresh = () => { ... }; creates a brand new function every time the component renders,
// potentially causing effects to re-run endlessly.
// The `refresh` reference stays stable as long as `db` (deps) does not change.
// In other words, **Only create a new refresh function if db changes.**
// Otherwise, reuse the same function object.
refresh depends on db. db comes from useSQLiteContext(). db is stable (doesn’t change), so refresh stays stable
src/hooks/usePrayers.tsThis hook is the single source of truth for everything related to prayer data and pagination. The component that uses it (PrayersScreen) becomes a pure render function: it receives data and callbacks, and it returns JSX. All the “how” lives inside the hook.
// src/hooks/usePrayers.ts
//
// PRAYERS HOOK
// ─────────────
// Manages all prayer data and page navigation state for the Prayers tab.
//
// WHAT IT PROVIDES
// prayer — the currently displayed prayer (full, with pages)
// loading — true while fetching from SQLite
// error — error message string, or null if no error
// currentPage — 0-based index into prayer.pages
// totalPages — prayer.pages.length (0 if no prayer)
// pageContent — the Page object at currentPage (or null)
// isFirstPage — true when currentPage === 0
// isLastPage — true when currentPage === totalPages - 1
// goNext() — advance to the next page (no-op on last page)
// goPrev() — go back to the previous page (no-op on first page)
// goToPage(n) — jump to page at index n
// refresh() — re-fetch today's prayer from SQLite
// loadById(id) — load a specific prayer by id_myprayer
//
// USAGE
// const prayers = usePrayers();
// // in JSX:
// <Text>{prayers.prayer?.title}</Text>
// <Button onPress={prayers.goNext} disabled={prayers.isLastPage} />
//
// WHY A HOOK INSTEAD OF STATE INSIDE THE COMPONENT?
// 1. Reusable: any screen can call usePrayers() and get the same logic, without copying code.
// 2. Testable: the hook can be tested in isolation (with a mock database) without rendering any UI
// and assert on the returned values.
// 3. Readable: the screen becomes a function that transforms the hook’s output into UI
// (a pure render function), which is much easier to read and maintain
// 4. Separable. Prevents stale data.
import { useState, useEffect, useCallback } from "react";
import { useSQLiteContext } from "expo-sqlite";
import {
getTodayPrayer,
getPrayerById,
listPrayers,
countPrayers,
} from "../db/prayerQueries";
import type { Prayer, PrayerSummary, Page } from "../db/types";
// ══════════════════════════════════════════════════════════════════════════════
// RETURN TYPE
// The shape of the object returned by usePrayers().
// Even though TypeScript could infer the shape from the return statement,
// defining an explicit interface gives us several advantages:
// (1) Documentation – a future developer can scan the interface and instantly see everything the hook provides.
// (2) Type safety at the consumer – the component gets autocompletion and compile‑time checks.
// (3) Testability – you can write unit tests that mock a UsePrayersResult without rendering any UI.
// ══════════════════════════════════════════════════════════════════════════════
export interface UsePrayersResult {
// ── Current prayer ──────────────────────────────────────────────────────────
/** The full prayer currently displayed (with pages). null if none loaded. */
prayer: Prayer | null;
// ── Page navigation ─────────────────────────────────────────────────────────
/** 0-based index of the currently displayed page. */
currentPage: number;
/** Total number of pages in the current prayer (0 if no prayer). */
totalPages: number;
/** The Page object at currentPage, or null if no prayer / no pages. */
pageContent: Page | null;
/** true when currentPage is 0 (on the first page). */
isFirstPage: boolean;
/** true when currentPage is the last page. */
isLastPage: boolean;
// ── Prayer list ──────────────────────────────────────────────────────────────
/** Lightweight list of all prayers (no page content) for the sidebar. */
prayerList: PrayerSummary[];
/** Total number of prayers in the database. */
prayerCount: number;
// ── Loading state ────────────────────────────────────────────────────────────
/** true while a database query is in progress. */
loading: boolean;
/** Error message if the last operation failed. null otherwise. */
error: string | null;
// ── Actions ──────────────────────────────────────────────────────────────────
/** Advance to the next page. No-op if already on the last page. */
goNext: () => void;
/** Go back to the previous page. No-op if already on the first page. */
goPrev: () => void;
/**
* Jump to a specific page by 0-based index.
* Silently clamps to valid range.
*/
goToPage: (index: number) => void;
/** Re-fetch today's prayer and reset to page 1. */
refresh: () => Promise<void>;
/**
* Load a specific prayer by its id_myprayer.
* Used when the user taps a prayer in the list.
*/
loadById: (id: number) => Promise<void>;
}
// ══════════════════════════════════════════════════════════════════════════════
// THE HOOK
// ══════════════════════════════════════════════════════════════════════════════
/**
* Manages prayer data and page navigation for the Prayers tab.
*
* Must be called inside a component that is a descendant of <QLiteProvider>.
* (The entire app is wrapped in <SQLiteProvider> in app/_layout.tsx.)
*
* @returns UsePrayersResult — all state and actions the Prayers tab needs
*/
export function usePrayers(): UsePrayersResult {
// Get the database connection from React context.
// This is equivalent to Python's self._db = PrayerDB() in __init__,
// but it comes from the shared SQLiteProvider rather than creating a new one.
// In other words, we never instantiate the database inside the hook; we just consume it!
const db = useSQLiteContext();
// ── State declarations ───────────────────────────────────────────────────────
// Each useState creates a state variable and a setter function.
// When a setter is called, React re-renders this component (and its children).
// More specifically, **React automatically queues a re‑render**. We never write self._render_page().
/** The full prayer with all its pages. null until loaded. */
const [prayer, setPrayer] = useState<Prayer | null>(null);
/** Which page the user is on (0-based index). */
const [currentPage, setCurrentPage] = useState<number>(0);
/** Lightweight list for the prayer sidebar (no page content). */
const [prayerList, setPrayerList] = useState<PrayerSummary[]>([]);
/** Total prayer count — shown as "X prayers" in the list header. */
const [prayerCount, setPrayerCount] = useState<number>(0);
/** true while any async operation is running. */
const [loading, setLoading] = useState<boolean>(true);
/** Error message from the last failed operation. null when everything is fine. */
const [error, setError] = useState<string | null>(null);
// ── Internal: load today's prayer + prayer list ──────────────────────────────
// useCallback memoises this function.
// The deps array [db] means: recreate the function only when `db` changes.
// Without useCallback, a new function would be created on every render,
// which would cause the useEffect below to re-run on every render → infinite loop !!!
const loadInitialData = useCallback(async () => {
/* Notice we start with loading = true. This means the first render shows a loading indicator,
which is a better UX than a blank screen. */
setLoading(true);
setError(null);
try {
// Loading data. Run queries (today’s prayer, the prayer list, and the count) in parallel
// — so the total wait time is roughly the longest of the three queries instead of their sum.
// Promise.all resolves when all promises resolve.
// If either fails, it throws and we catch below.
const [todayPrayer, list, count] = await Promise.all([
getTodayPrayer(db),
listPrayers(db, 100, 0),
countPrayers(db),
]);
setPrayer(todayPrayer);
/* React does not immediately mutate the variable prayer. Instead, React does this:
(1) React stores the new value (todayPrayer) in its internal state queue.
(2) React marks the component as “dirty” and squedules a re-render.
(3) During the next render, React calls your hook again: const [prayer, setPrayer] = useState(...) */
setPrayerList(list);
setPrayerCount(count);
/* After loading a new prayer, we always start on the first page. */
setCurrentPage(0);
} catch (e) {
setError(String(e));
} finally {
// setLoading(false) runs whether the try succeeded or the catch ran.
// Equivalent to Python's try: ... except: ... finally: ...
setLoading(false);
}
}, [db]);
// ── Effect: load data when the hook mounts ────────────────────────────────────
// useEffect runs AFTER the first render.
// The deps array [loadInitialData] means: re-run if loadInitialData changes.
// loadInitialData only changes when db changes (see useCallback above).
// In practice this runs exactly once — when the component first appears.
//
useEffect(() => {
loadInitialData();
}, [loadInitialData]);
// ── Derived values ────────────────────────────────────────────────────────────
// These are not stored in state because they can be computed on the fly from prayer and currentPage.
// React recalculates them on every render. That’s perfectly acceptable
// because the computations are cheap (just a few property accesses and comparisons).
const totalPages = prayer?.pages.length ?? 0;
const pageContent = prayer?.pages[currentPage] ?? null;
const isFirstPage = currentPage === 0;
const isLastPage = totalPages === 0 || currentPage === totalPages - 1;
// ── Actions ───────────────────────────────────────────────────────────────────
// These functions are exposed to the component, e.g., goNext is passed directly to a Button’s onPress prop.
// We wrap them in useCallback so the component's Prev/Next buttons do not
// get new function references on every render.
// By memoizing it, we keep the button’s props stable and avoid wasted renders.
const goNext = useCallback(() => {
if (currentPage < totalPages - 1) {
setCurrentPage((prev) => prev + 1);
// (prev) => prev + 1 is the functional update form.
// It reads the CURRENT value of currentPage at call time.
// Using setCurrentPage(currentPage + 1) can read a stale value
// if called multiple times before the next render.
// **This guarantees we are using the latest state value**,
// even if multiple setCurrentPage calls are queued before the next render.
}
}, [currentPage, totalPages]);
/* The same patterns apply to goPrev and goToPage */
const goPrev = useCallback(() => {
if (currentPage > 0) {
setCurrentPage((prev) => prev - 1);
}
}, [currentPage]);
const goToPage = useCallback((index: number) => {
// Clamp the imdex to avoid out‑of‑bounds errors
const clamped = Math.max(0, Math.min(index, totalPages - 1));
setCurrentPage(clamped);
}, [totalPages]);
/**
* Re-fetch today's prayer and reset to page 1.
* It simply calls loadInitialData again.
* Because loadInitialData already handles setting loading and resetting error,
* we get a clean refresh with one line of code.
*/
const refresh = useCallback(async () => {
await loadInitialData();
}, [loadInitialData]);
/**
* Load a specific prayer (that the user selected from the sidebar) by id_myprayer.
* It sets a new prayer and resets currentPage to 0.
* Note that it doesn’t update prayerList or prayerCount
* – those remain the same, so the sidebar doesn’t flicker or reload unnecessarily.
* refresh and loadById are wrapped in useCallback with their respective dependencies,
* so the button that triggers loadById doesn’t re‑render when unrelated state (like currentPage) changes.
*/
const loadById = useCallback(async (id: number) => {
setLoading(true);
setError(null);
try {
const loaded = await getPrayerById(db, id);
setPrayer(loaded);
setCurrentPage(0); // start on first page of the selected prayer
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
}, [db]);
// ── Return the public API ─────────────────────────────────────────────────────
/* The hook returns a bag of values and functions.
The component never reaches into the hook’s internals, never calls or touches the db directly, and never manages useState itself.
The component JUST USES these values and functions.
This is the separation of concerns:
the hook provides a clean API, and the component simply renders it. */
return {
// State
prayer,
currentPage,
totalPages,
pageContent,
isFirstPage,
isLastPage,
prayerList,
prayerCount,
loading,
error,
// Actions
goNext,
goPrev,
goToPage,
refresh,
loadById,
};
}
app/(tabs)/prayers.tsx with the hook test screenThis integrates the usePrayers hook to display a prayer’s content, navigate its pages, and switch between prayers in the library. This is a functional test screen.
This screen is the Prayers tab main view. It is a single React component (PrayersScreen) that renders everything from the usePrayers hook. There is no local state, no useEffect, and no direct database calls inside the component. All data and actions come from the hook.
// app/(tabs)/prayers.tsx
//
// PRAYERS TAB
//
import React from "react";
import {
View, Text, ScrollView, StyleSheet,
TouchableOpacity, ActivityIndicator, FlatList,
} from "react-native";
import { usePrayers } from "../../src/hooks/usePrayers";
export default function PrayersScreen() {
// One line: get everything the screen needs from the hook.
const {
prayer, // the currently selected prayer (or null if none)
loading, // true while initial fetch is in progress
error, // error message string or null
// currentPage, totalPages, pageContent – page navigation data
currentPage,
totalPages,
pageContent,
// isFirstPage, isLastPage – boundary flags for Prev/Next buttons
isFirstPage,
isLastPage,
prayerList,
prayerCount,
// goNext, goPrev, goToPage – page navigation
goNext,
goPrev,
goToPage,
refresh, // reloads today’s prayer
loadById, // switch to a specific prayer by id_myprayer
} = usePrayers();
// ── Loading state ────────────────────────────────────────────────────────────
// First state, renders a centered ActivityIndicator (gold spinner) and the text “Loading prayer…”.
if (loading) {
return (
<View style={s.center}>
<ActivityIndicator size="large" color="#c9a84c" />
<Text style={s.muted}>Loading prayer…</Text>
</View>
);
}
// ── Error state ──────────────────────────────────────────────────────────────
// Shows a red error title (❌ Error), the error message in monospace,
// and a Retry button that calls refresh.
if (error) {
return (
<View style={s.center}>
<Text style={s.errorTitle}>❌ Error</Text>
<Text style={s.errorText}>{error}</Text>
<TouchableOpacity style={s.btn} onPress={refresh}>
<Text style={s.btnText}>Retry</Text>
</TouchableOpacity>
</View>
);
}
// ── No prayer for today ──────────────────────────────────────────────────────
// Displays “No prayer for today.” with a hint.
// Refresh button is shown so the user can re‑fetch today’s prayer.
// Prayer list is rendered even in this state (mapped from prayerList),
// allowing the user to tap any existing prayer to load it via loadById.
if (!prayer) {
return (
<View style={s.center}>
<Text style={s.muted}>No prayer for today.</Text>
<Text style={s.muted}>
/* Contextual hint. If the DB has prayers → encourage the user to tap one.
If the DB is empty → instruct them to add one. */
{prayerCount > 0
? `${prayerCount} prayer(s) in library — tap one below.`
: "Add a prayer using the + button."}
</Text>
/* Refresh button. This calls the hook’s refresh() function, which re-fetches today's prayer. */
<TouchableOpacity style={[s.btn, { marginTop: 16 }]} onPress={refresh}>
<Text style={s.btnText}>↺ Refresh</Text>
</TouchableOpacity>
{/* Show prayer list even when no today's prayer (prayer === null). Tapping a row calls loadById(id) from the hook.
The hook loads the prayer and updates prayer. The screen re-renders and now shows the full prayer view. */}
{prayerList.map((p) => (
<TouchableOpacity
key={p.id_myprayer}
style={s.listRow}
onPress={() => loadById(p.id_myprayer)}
>
<Text style={s.listTitle}>{p.title}</Text>
<Text style={s.listDate}>{p.date || "no date"}</Text>
</TouchableOpacity>
))}
</View>
);
}
// ── Main prayer view ─────────────────────────────────────────────
// When a prayer is loaded, the screen splits into two panels (row layout).
return (
<View style={s.container}>
{/* LEFT: prayer list ─────────────────────────────────────────────────── */}
/* Prayer list: A FlatList rendering each item as a TouchableOpacity.
The currently active prayer is highlighted with a different background, left gold border, and gold text.
Each item shows the prayer’s title (max 2 lines) and its date.
A circular arrow (↺) at the bottom of the sidebar to reload today’s prayer. */
<View style={s.sidebar}>
<Text style={s.sidebarHeader}>
{prayerCount} prayer{prayerCount !== 1 ? "s" : ""}
</Text>
<FlatList
data={prayerList}
keyExtractor={(item) => String(item.id_myprayer)}
renderItem={({ item }) => (
<TouchableOpacity
style={[
s.sidebarItem,
// Highlight the currently selected prayer
item.id_myprayer === prayer.id_myprayer && s.sidebarItemActive,
]}
onPress={() => loadById(item.id_myprayer)}
>
<Text
style={[
s.sidebarTitle,
item.id_myprayer === prayer.id_myprayer && s.sidebarTitleActive,
]}
numberOfLines={2}
>
{item.title}
</Text>
<Text style={s.sidebarDate}>{item.date || "—"}</Text>
</TouchableOpacity>
)}
/>
<TouchableOpacity style={s.refreshBtn} onPress={refresh}>
<Text style={s.refreshBtnText}>↺</Text>
</TouchableOpacity>
</View>
{/* RIGHT: prayer detail ──────────────────────────────────────────────── */}
/* Main content (right panel). The main area is a View with padding and contains:
Prayer header: Title (gold, bold, 19px) and date (small monospace) displayed side‑by‑side.
Page indicator. Only rendered if totalPages > 0. “Page N of M” (1‑based) in blue, uppercase, slightly spaced letters.
Page content. Wrapped in a ScrollView for long content. If pageContent exists,
the HTML content is stripped of tags (a simple regex replaces H2 tags with “— text —” and removes all other tags).
A debug line shows page_id and page_number. If no pages exist, a muted “This prayer has no pages yet.” message is shown. */
<View style={s.main}>
{/* Prayer header */}
<View style={s.header}>
<Text style={s.title} numberOfLines={2}>{prayer.title}</Text>
<Text style={s.date}>{prayer.date}</Text>
</View>
<Text style={s.description}>{prayer.description}</Text>
{/* Page indicator */}
{totalPages > 0 && (
<Text style={s.pageIndicator}>
Page {currentPage + 1} of {totalPages}
</Text>
)}
{/* Page content */}
<ScrollView style={s.pageScroll} contentContainerStyle={s.pageContent}>
{pageContent ? (
<>
{/* Strip HTML tags for the test screen. */}
<Text style={s.pageText}>
{pageContent.content
.replace(/<h2>(.*?)<\/h2>/g, "— $1 —\n")
.replace(/<[^>]+>/g, "")
.trim()}
</Text>
{/* Debug info */}
<Text style={s.debugText}>
{`page_id=${pageContent.id} page_number=${pageContent.page_number}`}
</Text>
>
) : (
<Text style={s.muted}>This prayer has no pages yet.</Text>
)}
</ScrollView>
{/* Navigation buttons. Prev button: Left arrow, disabled (grey) when on the first page.
Page dots: A row of touchable dots for each page. The active dot is larger and gold. Tapping a dot calls goToPage(i).
Next button: Right arrow, disabled on the last page. */}
<View style={s.nav}>
<TouchableOpacity
style={[s.navBtn, isFirstPage && s.navBtnDisabled]}
onPress={goPrev}
disabled={isFirstPage}
>
<Text style={[s.navBtnText, isFirstPage && s.navBtnTextDisabled]}>
◀ Prev
</Text>
</TouchableOpacity>
{/* Page dots — tap to jump to page (it calls goToPage(i)) */}
<View style={s.pageDots}>
{Array.from({ length: totalPages }).map((_, i) => (
<TouchableOpacity
key={i}
onPress={() => goToPage(i)}
style={[s.dot, i === currentPage && s.dotActive]}
/>
))}
</View>
<TouchableOpacity
style={[s.navBtn, isLastPage && s.navBtnDisabled]}
onPress={goNext}
disabled={isLastPage}
>
<Text style={[s.navBtnText, isLastPage && s.navBtnTextDisabled]}>
Next ▶
</Text>
</TouchableOpacity>
</View>
{/* Chunk label */}
<Text style={s.chunkLabel}>
UsePrayers hook.
</Text>
</View>
</View>
);
}
const s = StyleSheet.create({
container: { flex: 1, flexDirection: "row", backgroundColor: "#1a1f2e" },
center: { flex: 1, backgroundColor: "#1a1f2e",
alignItems: "center", justifyContent: "center",
padding: 24, gap: 10 },
muted: { color: "#6b7280", fontSize: 13, textAlign: "center" },
errorTitle: { color: "#f87171", fontSize: 18, fontWeight: "bold" },
errorText: { color: "#f87171", fontSize: 12, fontFamily: "monospace",
textAlign: "center" },
// Sidebar
sidebar: { width: 170, backgroundColor: "#222840",
borderRightWidth: 1, borderRightColor: "#2d3550",
paddingTop: 12 },
sidebarHeader: { fontSize: 11, color: "#6b7280", fontWeight: "600",
paddingHorizontal: 12, marginBottom: 6,
textTransform: "uppercase", letterSpacing: 0.5 },
sidebarItem: { paddingHorizontal: 12, paddingVertical: 10,
borderBottomWidth: 1, borderBottomColor: "#1a1f2e" },
sidebarItemActive: { backgroundColor: "#1a1f2e",
borderLeftWidth: 2, borderLeftColor: "#c9a84c" },
sidebarTitle: { fontSize: 13, color: "#9ca3af", lineHeight: 18 },
sidebarTitleActive:{ color: "#c9a84c", fontWeight: "600" },
sidebarDate: { fontSize: 10, color: "#4b5563", marginTop: 2 },
refreshBtn: { padding: 12, alignItems: "center",
borderTopWidth: 1, borderTopColor: "#2d3550" },
refreshBtnText: { color: "#6b7280", fontSize: 16 },
// Main area
main: { flex: 1, padding: 16 },
header: { flexDirection: "row", justifyContent: "space-between",
alignItems: "flex-start", marginBottom: 4 },
title: { fontSize: 19, fontWeight: "bold", color: "#c9a84c",
flex: 1, marginRight: 8 },
date: { fontSize: 11, color: "#6b7280", fontFamily: "monospace" },
description: { fontSize: 13, color: "#9ca3af", marginBottom: 12,
lineHeight: 18 },
pageIndicator: { fontSize: 11, color: "#5b8fd4", fontWeight: "600",
marginBottom: 8, textTransform: "uppercase",
letterSpacing: 0.8 },
pageScroll: { flex: 1 },
pageContent: { paddingBottom: 16 },
pageText: { fontSize: 15, color: "#dde4f0", lineHeight: 24 },
debugText: { fontSize: 10, color: "#4b5563", marginTop: 12,
fontFamily: "monospace" },
// Navigation
nav: { flexDirection: "row", alignItems: "center",
paddingTop: 12, borderTopWidth: 1,
borderTopColor: "#2d3550", gap: 8 },
navBtn: { backgroundColor: "#5b8fd4", borderRadius: 6,
paddingVertical: 8, paddingHorizontal: 14 },
navBtnDisabled: { backgroundColor: "#2d3550" },
navBtnText: { color: "#ffffff", fontSize: 13, fontWeight: "600" },
navBtnTextDisabled:{ color: "#6b7280" },
pageDots: { flex: 1, flexDirection: "row",
justifyContent: "center", gap: 6, flexWrap: "wrap" },
dot: { width: 8, height: 8, borderRadius: 4,
backgroundColor: "#2d3550" },
dotActive: { backgroundColor: "#c9a84c", width: 10, height: 10,
borderRadius: 5 },
chunkLabel: { fontSize: 10, color: "#4b5563", marginTop: 8,
textAlign: "center" },
// List / buttons (used in no-prayer state)
btn: { backgroundColor: "#5b8fd4", borderRadius: 8,
padding: 12, alignItems: "center" },
btnText: { color: "#fff", fontSize: 14, fontWeight: "600" },
listRow: { backgroundColor: "#222840", borderRadius: 8,
padding: 12, marginTop: 8, flexDirection: "row",
justifyContent: "space-between", width: "100%" },
listTitle: { fontSize: 13, color: "#dde4f0", flex: 1 },
listDate: { fontSize: 11, color: "#6b7280" },
});
npx expo start --clear