diff --git a/assets/css/app.css b/assets/css/app.css
index d1bb280..0c1d8c1 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -3,20 +3,23 @@
"Times New Roman", serif;
}
+/* Only A-Z and a-z: the compiler puts a drop cap on a chapter only when it
+ opens on a letter. */
+@font-face {
+ font-family: "Day Leaf Initials";
+ src: url("../fonts/DayLeafInitials.woff2") format("woff2");
+ font-display: swap;
+}
+
/* Both of these are links to their own id. They keep the text colour they
had as plain numbers, since an underlined blue verse number in the middle
of a sentence reads as part of the text. */
.v,
.chapter-number a {
- color: var(--bs-secondary-color);
+ color: var(--bs-body-color);
text-decoration: none;
}
-.v:hover,
-.chapter-number a:hover {
- color: var(--bs-emphasis-color);
-}
-
.v {
font-size: .7em;
font-weight: 700;
@@ -33,13 +36,99 @@
border-radius: 2px;
}
+/* Three lines deep. The verse number it replaces is still there for its id
+ and the keyboard, as the chapter number above already says where verse 1
+ is -- the way a printed Bible drops it. */
+.dropcap {
+ float: left;
+ font-family: "Day Leaf Initials", var(--bs-body-font-family);
+ font-size: 5em;
+ line-height: .8;
+ margin: .06em .08em 0 0;
+}
+
+/* The first line pulled in closer to the letter than the lines below it, so
+ the rest of the first word reads as part of it. .chapter makes this beat
+ the list's and the poetry's own first-line indents. */
+.chapter :is(p, li):has(.dropcap) { text-indent: -.4em; }
+
+/* The rest of the first line in small caps, as a lead-in from the initial.
+ The letter can sit inside a span, such as .wj. */
+:is(p, li):has(.dropcap)::first-line {
+ font-variant: small-caps;
+ letter-spacing: .05em;
+}
+
+.v:has(+ .dropcap):not(:focus-visible) {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+
+/* The book's title, centred with room around it. --u shrinks with the header
+ on a phone. */
+.book-header {
+ --u: min(1rem, 100cqi / 22);
+ container-type: inline-size;
+ display: grid;
+ place-items: center;
+ padding: calc(var(--u) * 4.5) calc(var(--u) * 3.5);
+ margin: 1rem 0 2rem;
+}
+
+.book-header h1 {
+ max-width: 100%;
+ margin: 0;
+ padding: .6em 1.2em;
+ text-align: center;
+ font-weight: 400;
+ line-height: 1.15;
+ text-wrap: balance;
+}
+
+/* The lines above and below the name itself, such as "The First Book of
+ Moses, Commonly Called", on lines of their own in small caps. */
+.book-header :is(.mt1, .mt2, .mt3, .mt4) {
+ display: block;
+ font-size: .45em;
+ font-variant: small-caps;
+ letter-spacing: .08em;
+ margin: .3em 0;
+}
+
+.book-header .mt { display: block; }
+
+/* Centred between two flourishes (Group L 49), the left one mirrored so
+ their curls point in at the number. */
.chapter-number {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: .4em;
font-size: 2rem;
font-weight: 400;
color: var(--bs-secondary-color);
- margin: 2rem 0 .75rem;
+ margin: 5rem 0 .75rem;
}
+.chapter-number::before,
+.chapter-number::after {
+ content: "";
+ flex: none;
+ width: 3.2em;
+ height: .8em;
+ /* Flex centres on the line box, which leaves room below for descenders;
+ numerals have none, so this lifts the flourish to their middle. */
+ translate: 0 -.1em;
+ background: currentColor;
+ mask: url("../ornaments/group-l-49.svg") center / contain no-repeat;
+}
+
+.chapter-number::before { transform: scaleX(-1); }
+
/* The export marks these up as list items but hides the marker, indenting the
first line instead, which is how a printed Bible sets them -- Ezra 10:18-44
and the like are prose, not a bulleted list. Only the glossary and front
@@ -66,6 +155,12 @@
.poetry .line[data-indent="2"] { padding-left: 3em; }
.poetry .line[data-indent="3"] { padding-left: 4.5em; }
+/* A poetry line's indent is padding, which a float covers. Each line after
+ the drop cap becomes its own block beside the letter, so its indent counts
+ from the letter's edge; the drop cap's own line has no indent of its own. */
+.poetry .line:has(.dropcap) { padding-left: 0; }
+.poetry .line:not(:has(.dropcap)) { display: flow-root; }
+
.d {
font-style: italic;
color: var(--bs-secondary-color);
diff --git a/assets/fonts/DayLeafInitials.woff2 b/assets/fonts/DayLeafInitials.woff2
new file mode 100644
index 0000000..19dbe8c
Binary files /dev/null and b/assets/fonts/DayLeafInitials.woff2 differ
diff --git a/assets/ornaments/group-l-49.svg b/assets/ornaments/group-l-49.svg
new file mode 100644
index 0000000..a008c35
--- /dev/null
+++ b/assets/ornaments/group-l-49.svg
@@ -0,0 +1 @@
+
diff --git a/assets/sw.js b/assets/sw.js
index 09512f7..31a5dbc 100644
--- a/assets/sw.js
+++ b/assets/sw.js
@@ -23,6 +23,8 @@ const PRECACHE = [
'./manifest.webmanifest',
'./css/app.css',
'./css/bootstrap.min.css',
+ './fonts/DayLeafInitials.woff2',
+ './ornaments/group-l-49.svg',
'./js/htmx.min.js',
'./js/bootstrap.bundle.min.js'
].concat(TEMPLATE_URLS);
diff --git a/server.py b/server.py
index cd46a51..f506610 100644
--- a/server.py
+++ b/server.py
@@ -44,6 +44,7 @@ class Handler(SimpleHTTPRequestHandler):
".png": "image/png",
".svg": "image/svg+xml",
".ico": "image/x-icon",
+ ".woff2": "font/woff2",
}
server_version = "bible-static"
sys_version = ""
diff --git a/utils/compile_bible.py b/utils/compile_bible.py
index c01d11e..d9cc46a 100644
--- a/utils/compile_bible.py
+++ b/utils/compile_bible.py
@@ -453,11 +453,51 @@ def ref_href(node, book, chapter):
return "%s#%s" % (base, verse_id(target_ch, verse))
return "%s#ch%d" % (base, target_ch)
+DROPCAP_BLOCKS = ("paragraph", "line", "item")
+
+def with_dropcap(nodes):
+ """Split the first letter of the first text off into a dropcap node.
+
+ Returns the new node list, or None when the text does not start with a
+ letter. The initials font has only A-Z and a-z, so a chapter opening on
+ a quotation mark or a digit gets no drop cap rather than a fallback one.
+ """
+ nodes = list(nodes)
+ for i, node in enumerate(nodes):
+ kind = node["t"]
+ if kind == "verse":
+ continue
+ # A note caller would land between the letter and the rest of its word.
+ if kind == "noteref":
+ return None
+ if kind == "text":
+ text = node["v"].lstrip()
+ if not text:
+ continue
+ if not ("A" <= text[0] <= "Z" or "a" <= text[0] <= "z"):
+ return None
+ nodes[i:i + 1] = [{"t": "dropcap", "v": text[0]},
+ dict(node, v=text[1:])]
+ else:
+ inner = with_dropcap(node.get("c", []))
+ if inner is None:
+ return None
+ nodes[i] = dict(node, c=inner)
+ # The verse number before the drop cap would otherwise sit between
+ # the floated letter and the rest of its word.
+ for j in range(i):
+ if nodes[j]["t"] == "verse":
+ nodes[j] = dict(nodes[j], dropcap=True)
+ return nodes
+ return None
+
def render_inline(nodes, book, chapter, out):
for node in nodes:
kind = node["t"]
if kind == "text":
out.append(esc(node["v"]))
+ elif kind == "dropcap":
+ out.append('%s' % esc(node["v"]))
elif kind == "verse":
label = esc(node["label"])
vid = verse_id(chapter, node["label"])
@@ -466,10 +506,13 @@ def render_inline(nodes, book, chapter, out):
# that dialog. Being a real link is also what gives it the
# keyboard and context menu a reader expects.
out.append('%s '
+ 'aria-label="Verse %s, options">%s'
% (vid, vid, label, label))
+ # A non-breaking space keeps the number on the same line as the
+ # word it introduces, as the source does. Before a drop cap the
+ # number is hidden and the space would only push the word away.
+ if not node.get("dropcap"):
+ out.append(" ")
elif kind == "noteref":
n = node["id"].replace("FN", "")
out.append(''
@@ -550,8 +593,14 @@ def render_chapter(chapter, book, out):
out.append(" \n")
group = None
+ dropcap = True
for block in chapter["blocks"]:
btype = block["type"]
+ if dropcap and btype in DROPCAP_BLOCKS:
+ dropcap = False
+ content = with_dropcap(block.get("content", []))
+ if content is not None:
+ block = dict(block, content=content)
if btype == "break":
close_group()
continue