Files
bible/assets/css/app.css
T
2026-10-24 09:54:42 -04:00

677 lines
19 KiB
CSS

/* Colours and type for the whole app. The values are the ones Bootstrap
gave the page before it was taken out, so nothing shifts. */
/* Variable fonts, one file per style covering weights 400 to 800. They are
cut down to Latin, Greek and punctuation; Hebrew falls back to the next
font in the stack. */
@font-face {
font-family: "EB Garamond";
src: url("../fonts/EBGaramond.woff2") format("woff2");
font-weight: 400 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "EB Garamond";
src: url("../fonts/EBGaramond-Italic.woff2") format("woff2");
font-weight: 400 800;
font-style: italic;
font-display: swap;
}
:root {
--font-body: "EB Garamond", Georgia, "Iowan Old Style", "Palatino Linotype",
"Times New Roman", serif;
--text: #212529;
--text-muted: rgb(33 37 41 / .75);
--background: #f8f9fa;
--link: #0d6efd;
--link-hover: #0a58ca;
--focus: #0d6efd;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
@media (prefers-reduced-motion: no-preference) {
:root { scroll-behavior: smooth; }
}
*,
*::before,
*::after { box-sizing: border-box; }
/* flow-root keeps main's top margin inside the body. Let through, it moves
the body's top edge down the page, and htmx scrolls a boosted page to that
edge, not to the very top. EB Garamond's x-height is a fifth smaller than
Georgia's, so it is set a size up to read the same. */
body {
display: flow-root;
margin: 0;
font-family: var(--font-body);
font-size: 1.1875rem;
line-height: 1.55;
color: var(--text);
background: var(--background);
-webkit-tap-highlight-color: transparent;
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 .5rem;
font-weight: 500;
line-height: 1.2;
}
/* Headings grow with the viewport up to their full size on a wide screen. */
h1 { font-size: calc(1.375rem + 1.5vw); }
h2 { font-size: calc(1.325rem + .9vw); }
h3 { font-size: calc(1.3rem + .6vw); }
h4 { font-size: calc(1.275rem + .3vw); }
h5 { font-size: 1.25rem; }
h6 { font-size: 1rem; }
@media (min-width: 1200px) {
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.75rem; }
h4 { font-size: 1.5rem; }
}
p,
ul,
ol { margin: 0 0 1rem; }
ul,
ol { padding-left: 2rem; }
:is(ul, ol) :is(ul, ol) { margin-bottom: 0; }
/* Footnote callers sit above the line without pushing it apart. */
sup {
position: relative;
top: -.5em;
font-size: .75em;
line-height: 0;
vertical-align: baseline;
}
a { color: var(--link); }
a:hover { color: var(--link-hover); }
/* The frame's measures, shared by the sidebar and the button that opens it
below a laptop. A corner is six rules square, set turned half round from how
the piece is drawn, so its small square sits outermost. The frame runs two
rules along each side, one on the edge and one 2.5 rules in, both meeting
the corner's far side. */
.sidebar-shell {
--rule: 3px;
--corner: calc(var(--rule) * 6);
--inner: calc(var(--rule) * 2.5);
--run: calc(100% - var(--corner) * 2);
--margin: 1.5rem;
}
/* Over the text along the left edge, so the reading column stays centred on
the screen, with the same margin on every side. The dialog's own box has
no padding: a click that lands on it rather than on what is inside is a
click on the backdrop. */
.sidebar {
position: fixed;
inset: var(--margin) auto var(--margin) var(--margin);
z-index: 1;
width: auto;
height: auto;
max-height: none;
margin: 0;
padding: 0;
color: var(--text);
border: 0;
background:
url("../ornaments/uniform-rule-corner-br.svg") left top / var(--corner) no-repeat,
url("../ornaments/uniform-rule-corner-bl.svg") right top / var(--corner) no-repeat,
url("../ornaments/uniform-rule-corner-tr.svg") left bottom / var(--corner) no-repeat,
url("../ornaments/uniform-rule-corner-tl.svg") right bottom / var(--corner) no-repeat,
linear-gradient(#000 0 0) var(--corner) var(--inner) / var(--run) var(--rule) no-repeat,
linear-gradient(#000 0 0) var(--corner) 0 / var(--run) var(--rule) no-repeat,
linear-gradient(#000 0 0) var(--corner) calc(100% - var(--inner)) / var(--run) var(--rule) no-repeat,
linear-gradient(#000 0 0) var(--corner) 100% / var(--run) var(--rule) no-repeat,
linear-gradient(#000 0 0) var(--inner) var(--corner) / var(--rule) var(--run) no-repeat,
linear-gradient(#000 0 0) 0 var(--corner) / var(--rule) var(--run) no-repeat,
linear-gradient(#000 0 0) calc(100% - var(--inner)) var(--corner) / var(--rule) var(--run) no-repeat,
linear-gradient(#000 0 0) 100% var(--corner) / var(--rule) var(--run) no-repeat,
var(--background);
transition: opacity .15s ease-out;
}
/* Where the reading column has a margin beside it, the sidebar fills it, the
gap to the text the same as the margin round the rest. */
@media (min-width: 1200px) {
.sidebar {
width: calc((100% - var(--measure)) / 2 + var(--gutter) - var(--margin) * 2);
}
}
/* The title over a rule, then the selects, then the link back to the index
pushed down to the foot of the frame by the row between that takes up the
rest. */
.sidebar-body {
display: grid;
grid-template-rows: auto auto auto 1fr auto;
height: 100%;
padding: calc(var(--corner) + 1rem);
}
.sidebar-title {
font-size: 1.5rem;
text-align: center;
}
/* A hairline, like the selects' boxes. */
.sidebar-rule {
width: 100%;
margin: 0 0 1rem;
border: 0;
border-top: 1px solid #000;
}
.sidebar-index {
grid-row: 5;
justify-self: center;
}
.sidebar::backdrop { background: rgb(0 0 0 / .3); }
/* Away from the top it fades out unless it is under the pointer. Focus does
not hold it: a select keeps focus after it is used, which would leave the
sidebar over the text. */
.scrolled .sidebar:not(:modal, :hover) { opacity: 0; }
/* Below a laptop the sidebar waits behind a button in the corner, a box in the
frame's rule. Opened, the frame grows out of it and a copy of it, which
closes the dialog, stands in for the frame's bottom right corner. */
.sidebar-fab {
display: none;
width: 4rem;
height: 4rem;
padding: 0;
color: #000;
border: var(--rule) solid #000;
background: var(--background);
cursor: pointer;
}
.sidebar-fab::before {
content: "";
display: block;
width: 90%;
height: 90%;
margin: auto;
background: currentColor;
mask: url("../ornaments/mural-311a.svg") center / contain no-repeat;
}
.sidebar-fab:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.sidebar-open {
position: fixed;
right: max(var(--margin), env(safe-area-inset-right));
bottom: max(var(--margin), env(safe-area-inset-bottom));
z-index: 1;
transition: opacity .15s ease-out;
}
/* It fades out away from the top of the page but can still be tapped. Only a
real pointer brings it back on hover: a touch screen keeps :hover on
whatever was last tapped, which would leave it showing. */
.scrolled .sidebar-open:not(:focus-visible) { opacity: 0; }
@media (hover: hover) {
.scrolled .sidebar-open:hover { opacity: 1; }
}
.sidebar-close {
position: absolute;
right: 0;
bottom: 0;
}
@media (max-width: 767.98px) {
.sidebar-shell { --margin: 1rem; }
}
/* Without a margin beside the reading column to hold the sidebar, it waits
behind the button. Opened, it fills the screen less the margin all round,
clear of a notch or home indicator. */
@media (max-width: 1199.98px) {
.sidebar:not(:modal) { display: none; }
.sidebar-open,
.sidebar:modal .sidebar-close { display: block; }
.sidebar:modal {
inset: max(var(--margin), env(safe-area-inset-top))
max(var(--margin), env(safe-area-inset-right))
max(var(--margin), env(safe-area-inset-bottom))
max(var(--margin), env(safe-area-inset-left));
width: auto;
max-width: none;
}
}
/* Each label on the left with its select set flush to the right. */
.reference {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: .5rem 1rem;
}
/* A plain box in a hairline, square like the frame, with a small black
triangle in place of the system's arrow. */
.reference select {
appearance: none;
padding: .25rem 1.75rem .25rem .5rem;
font: inherit;
color: #000;
border: 1px solid #000;
border-radius: 0;
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0h10L5 6z'/%3E%3C/svg%3E")
right .5rem center / .625rem no-repeat,
var(--background);
cursor: pointer;
}
.reference select:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
/* One reading column: the full width less a gutter on anything narrower than
a laptop, and a measure that grows a step with the screen beyond that. The
sidebar sizes itself from the same two values. */
:root { --gutter: .75rem; }
main {
margin: 1.5rem auto;
padding: 0 var(--gutter);
}
@media (min-width: 1200px) {
:root { --measure: 760px; }
main { max-width: var(--measure); }
}
@media (min-width: 1400px) {
:root { --measure: 880px; }
}
/* 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(--text);
text-decoration: none;
}
/* Garamond's numbers are old-style by default; in a superscript or beside
the flourishes they sink below the line, so these use lining ones. */
.v,
.chapter-number { font-variant-numeric: lining-nums; }
.v {
font-size: .7em;
font-weight: 700;
vertical-align: super;
line-height: 0;
margin-right: .15em;
user-select: none;
}
.v:focus-visible,
.chapter-number a:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
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(--font-body);
font-size: 5em;
line-height: .8;
margin: .1em .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 {
container-type: inline-size;
display: grid;
place-items: center;
margin: 10rem 0;
}
.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(--text);
margin: 10rem 0 .75rem;
}
.chapter-number::before,
.chapter-number::after {
content: "";
flex: none;
width: 3.2em;
height: .8em;
/* Flex centres on the line box. In EB Garamond its middle is .35em above
the baseline and a lining numeral's is .33em, so this lowers the
flourish the difference to sit level with the number. */
translate: 0 .05em;
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
matter items it actually marks, with a square. The list markup is worth
keeping for what it means; the bullet is not. */
.list {
list-style: none;
padding-left: 0;
}
.list .li { text-indent: 2em; }
.list .ili {
list-style: square;
margin-left: 2em;
}
.poetry { margin-bottom: 1rem; }
.poetry .line {
margin: 0;
padding-left: 1.5em;
text-indent: -1.5em;
}
.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(--text-muted);
}
.footnotes {
font-size: .875em;
color: var(--text-muted);
}
.footnotes-title { font-size: 1em; }
.marker { font-weight: 700; }
.noteref,
.marker { font-variant-emoji: text; }
.chapter,
.v { scroll-margin-top: 1rem; }
.index ul {
list-style: none;
padding-left: 0;
}
.index .count { display: none; }
.book .p,
.book .m,
.book .pi,
.book .ip,
.book .li,
.book .ili,
.footnotes .note {
text-align: justify;
text-justify: inter-word;
-webkit-hyphens: auto;
hyphens: auto;
text-wrap: pretty;
}
@media (max-width: 767.98px) {
body { font-size: 1.0625rem; }
.book { line-height: 1.5; }
.book .p,
.book .m,
.book .pi,
.book .ip,
.book .li,
.book .ili,
.footnotes .note { word-spacing: -.04em; }
}
.poetry .line,
.book .sp,
.book .d {
text-align: left;
-webkit-hyphens: manual;
hyphens: manual;
}
/* The index's frame, set in the Schuil tiles' own units, a pica to 100. A
unit is the specimen's size where the reading column is at its narrowest
on a laptop, and smaller below that, so the band of heads under the title
always fits with a little brass either side. */
.home { container-type: inline-size; }
.schuil {
--u: min(.22px, 100cqw / 2700);
position: relative;
min-height: calc(var(--u) * 2600);
padding: calc(var(--u) * 1600) calc(var(--u) * 300) calc(var(--u) * 450);
}
.schuil-ornaments {
position: absolute;
inset: 0;
color: #000;
pointer-events: none;
}
.schuil-half {
position: absolute;
inset: 0 50% 0 0;
}
.schuil-right {
inset: 0 0 0 50%;
transform: scaleX(-1);
}
/* A tile goes where the specimen put it: the point (--x, --y) from the
corner it hangs from, then turned by --m about its own corner, as an SVG
transform would. */
.schuil-tile {
position: absolute;
left: calc(var(--ax, 0) * 100%);
top: calc(var(--ay, 0) * 100%);
width: calc(var(--w) * var(--u));
height: calc(var(--h) * var(--u));
transform-origin: 0 0;
transform: translate(calc(var(--x) * var(--u)), calc(var(--y) * var(--u)))
matrix(var(--m, 1, 0, 0, 1, 0, 0));
background: currentColor;
mask-position: center;
mask-size: 100% 100%;
mask-repeat: no-repeat;
}
.schuil-01 { --w: 50; --h: 50; mask-image: url("../ornaments/schuil-01.svg"); }
.schuil-06 { --w: 100; --h: 100; mask-image: url("../ornaments/schuil-06.svg"); }
.schuil-10 { --w: 150; --h: 150; mask-image: url("../ornaments/schuil-10.svg"); }
.schuil-11 { --w: 150; --h: 125; mask-image: url("../ornaments/schuil-11.svg"); }
.schuil-12 { --w: 150; --h: 125; mask-image: url("../ornaments/schuil-12.svg"); }
.schuil-13 { --w: 250; --h: 300; mask-image: url("../ornaments/schuil-13.svg"); }
.schuil-14 { --w: 300; --h: 600; mask-image: url("../ornaments/schuil-14.svg"); }
.schuil-15 { --w: 350; --h: 350; mask-image: url("../ornaments/schuil-15.svg"); }
.schuil-16 { --w: 350; --h: 700; mask-image: url("../ornaments/schuil-16.svg"); }
.schuil-brass-v { --w: 50; --h: 100; mask-image: url("../ornaments/schuil-brass-v.svg"); }
/* Brass rule that stretches: two 8 unit strokes in a six point body, drawn
as gradients so they run any length without a seam. A horizontal run
stands in from either side by --x, or by --r on the right; a vertical one
from the head by --y and the foot by --b. */
.schuil-rule {
position: absolute;
left: calc(var(--x) * var(--u));
right: calc(var(--r, var(--x)) * var(--u));
top: calc(var(--ay, 0) * 100% + var(--y) * var(--u));
height: calc(var(--u) * 50);
background:
linear-gradient(currentColor 0 0) 0 calc(var(--u) * 8) / 100% calc(var(--u) * 8) no-repeat,
linear-gradient(currentColor 0 0) 0 calc(var(--u) * 34) / 100% calc(var(--u) * 8) no-repeat;
}
.schuil-rule-v {
position: absolute;
left: calc(var(--x) * var(--u));
top: calc(var(--y) * var(--u));
bottom: calc(var(--b) * var(--u));
width: calc(var(--u) * 50);
background:
linear-gradient(currentColor 0 0) calc(var(--u) * 8) 0 / calc(var(--u) * 8) 100% no-repeat,
linear-gradient(currentColor 0 0) calc(var(--u) * 34) 0 / calc(var(--u) * 8) 100% no-repeat;
}
/* Set in the panel at the head, the panel's inside 2100 units by 400 at
the specimen's size. */
.schuil-title {
position: absolute;
inset: calc(var(--u) * 475) calc(var(--u) * 475) auto;
height: calc(var(--u) * 400);
display: flex;
align-items: center;
justify-content: center;
margin: 0;
font-size: calc(var(--u) * 220);
font-weight: 400;
line-height: 1;
letter-spacing: .06em;
text-transform: uppercase;
}
.app-version {
margin: 3rem 0 .5rem;
text-align: center;
}
.app-version p {
margin: .25rem 0 0;
font-size: .875em;
color: var(--text-muted);
}
/* The page shown while the service worker installs. */
.boot {
margin: 0 auto;
padding: 3rem .75rem;
text-align: center;
}
.boot p { color: var(--text-muted); }
.spinner {
display: inline-block;
width: 2rem;
height: 2rem;
margin-bottom: 1rem;
border: .25em solid #6c757d;
border-right-color: transparent;
border-radius: 50%;
animation: spin .75s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.boot .boot-warning {
display: inline-block;
padding: 1rem;
color: #664d03;
background: #fff3cd;
border: 1px solid #ffe69c;
border-radius: .375rem;
}