/* ————————————————————————————————————————————————————————————
Cavu Reader — Clear Air.
CAVU: ceiling and visibility unlimited. High-altitude blues,
a horizon line, drifting haze. Body prose in Literata (drawn
for long-form ebook reading), book display in Fraunces, app
chrome in Jost (geometric, flight-deck).
Day/night: tokens are defined for light, overridden by the
prefers-color-scheme media query, and pinned either way by an
explicit data-theme attribute on (the toggle).
———————————————————————————————————————————————————————————— */
:root {
--paper: #f4f7fb;
--ink: #1c2836;
--muted: #5b6b7e;
--accent: #2456a6;
--group: #9a5b0e;
--rule: #d3deea;
--wash: #e7eef6;
--card: #ffffff;
--sky-zenith: #3f6fb4;
--sky-mid: #a3c3e4;
--sky-horizon: #eaf1f8;
--hero-ink: #17273c;
--hero-top-ink: #f2f6fb;
--haze-o: 1;
--flock-o: 0.95;
--font-body: 'Literata', Georgia, 'Times New Roman', serif;
--font-display: 'Fraunces', Georgia, serif;
--font-ui: 'Jost', system-ui, -apple-system, 'Segoe UI', sans-serif;
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #0d141f;
--ink: #dbe4ef;
--muted: #8496ac;
--accent: #85b4ea;
--group: #e2ac63;
--rule: #223244;
--wash: #131d2b;
--card: #131d2b;
--sky-zenith: #060b18;
--sky-mid: #1a3a52;
--sky-horizon: #2b4a78;
--hero-ink: #e8eff8;
--hero-top-ink: #e8eff8;
--haze-o: 0.35;
--flock-o: 0.5;
}
}
:root[data-theme='light'] {
--paper: #f4f7fb;
--ink: #1c2836;
--muted: #5b6b7e;
--accent: #2456a6;
--group: #9a5b0e;
--rule: #d3deea;
--wash: #e7eef6;
--card: #ffffff;
--sky-zenith: #3f6fb4;
--sky-mid: #a3c3e4;
--sky-horizon: #eaf1f8;
--hero-ink: #17273c;
--hero-top-ink: #f2f6fb;
--haze-o: 1;
--flock-o: 0.95;
color-scheme: light;
}
:root[data-theme='dark'] {
--paper: #0d141f;
--ink: #dbe4ef;
--muted: #8496ac;
--accent: #85b4ea;
--group: #e2ac63;
--rule: #223244;
--wash: #131d2b;
--card: #131d2b;
--sky-zenith: #060b18;
--sky-mid: #1a3a52;
--sky-horizon: #2b4a78;
--hero-ink: #e8eff8;
--hero-top-ink: #e8eff8;
--haze-o: 0.35;
--flock-o: 0.5;
color-scheme: dark;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a {
color: var(--accent);
}
::selection {
background: color-mix(in srgb, var(--accent) 22%, transparent);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ——— Prose ——————————————————————————————————————————————— */
/* The author's typesetting arrives as data-* hooks and custom properties
on this container (lib/typeset typesetAttrs); every rule below has a
default that matches what the book looked like before styling existed,
so a book with no page.cavu.book#styling renders unchanged. */
.prose {
font-family: var(--font-body);
font-size: 1.0625rem;
line-height: 1.78;
orphans: 2;
widows: 2;
}
.prose p {
margin: 0 0 1.1em;
}
.prose .chapter-title,
.prose h1 {
font-family: var(--font-display);
font-weight: 560;
font-size: 1.9rem;
line-height: 1.15;
letter-spacing: -0.01em;
margin: 0 0 1.4em;
}
.prose h2,
.prose h3,
.prose h4 {
font-family: var(--font-display);
font-weight: 560;
line-height: 1.2;
margin: 1.6em 0 0.7em;
}
.prose blockquote {
margin: 1.4em 0 1.4em 1.2em;
padding-left: 1.1em;
border-left: 2px solid var(--rule);
color: color-mix(in srgb, var(--ink) 78%, var(--muted));
font-style: italic;
}
.prose hr {
border: 0;
border-top: 1px solid var(--rule);
width: 4.5em;
margin: 2.4em auto;
}
/* Lists. Item text is set directly in the
(never wrapped in a
paragraph), so the prose indent and justification rules pass over it
and only these apply. */
.prose ul,
.prose ol {
margin: 1.2em 0;
padding-left: 1.8em;
}
.prose li {
margin: 0.3em 0;
}
/* A nested list belongs to the item above it, not to the page. */
.prose li > ul,
.prose li > ol {
margin: 0.3em 0 0.4em;
}
.prose figure {
margin: 1.8em 0;
}
.prose img {
max-width: 100%;
height: auto;
display: block;
}
.prose code {
font-size: 0.9em;
background: var(--wash);
padding: 0.1em 0.35em;
border-radius: 4px;
}
/* Indented paragraphs: set solid, first line indented — the book idiom.
Anything that already announces a new thought (a heading, a break, a
quotation) resets the indent, since the space above it does the work. */
.prose[data-paragraphs='indent'] p {
margin: 0;
text-indent: 1.3em;
}
.prose[data-paragraphs='indent'] .chapter-title + p,
.prose[data-paragraphs='indent'] h2 + p,
.prose[data-paragraphs='indent'] h3 + p,
.prose[data-paragraphs='indent'] h4 + p,
.prose[data-paragraphs='indent'] hr + p,
.prose[data-paragraphs='indent'] blockquote + p,
.prose[data-paragraphs='indent'] figure + p,
.prose[data-paragraphs='indent'] ul + p,
.prose[data-paragraphs='indent'] ol + p,
.prose[data-paragraphs='indent'] .verse + p,
.prose[data-paragraphs='indent'] .page-break + p,
.prose[data-paragraphs='indent'] .block-edit-void + p,
.prose[data-paragraphs='indent'] p.scene-break,
.prose[data-paragraphs='indent'] p.scene-break + p {
text-indent: 0;
}
/* Justification. Hyphenation is not optional here — an unhyphenated
justified column at this measure opens rivers. */
.prose[data-justify='on'] p,
.prose[data-justify='on'] blockquote {
text-align: justify;
hyphens: auto;
}
/* Chapter openings. The initial is drawn in the display face so it reads
as part of the title's voice rather than the body's. */
.prose[data-chapter-initial='dropCap'] .chapter-title + p::first-letter {
float: left;
font-family: var(--font-display);
font-weight: 560;
font-size: 3.1em;
line-height: 0.82;
padding: 0.06em 0.08em 0 0;
}
.prose[data-chapter-initial='raised'] .chapter-title + p::first-letter {
font-family: var(--font-display);
font-weight: 560;
font-size: 2.1em;
line-height: 1;
padding-right: 0.03em;
}
.prose[data-chapter-initial='smallCaps'] .chapter-title + p::first-line {
font-variant-caps: small-caps;
letter-spacing: 0.03em;
}
/* An explicit page break. In a continuous flow there is no page to break
to, so it reads as a rest; paginated reading and the exports turn it
into a real break (see .paginated-content below, and export/epub.ts). */
.prose .page-break {
height: 1.6em;
margin: 0;
}
/* Scene breaks. An ornament is a paragraph (a rule element can't carry
text); a drawn rule stays the
it always was, and `blank` keeps the
air but hides the line. */
.prose p.scene-break {
margin: 2.4em 0;
text-align: center;
text-indent: 0;
font-size: 1.25em;
line-height: 1;
color: var(--muted);
}
.prose[data-scene-break='blank'] hr.scene-break {
border-top-color: transparent;
}
/* Verse: hard line breaks, stanzas kept together, set like a song. */
.verse {
margin: 1.6em 0 1.6em 2.2em;
font-style: italic;
}
.verse .stanza {
margin: 0 0 1.2em;
break-inside: avoid;
/* Lines arrive as \n in one RichText run (facet offsets span them). */
white-space: pre-line;
}
/* ——— Screenplays ——————————————————————————————————————————— */
/* A book whose form is screenplay (lib/typeset forces Courier Prime into
--font-body/--font-display). The elements take the standard US script
page's indents (lib/screenplay SCREENPLAY_ELEMENTS), in characters of a
60-character line. A reading column is rarely exactly that wide, so each
indent is min(its ch value, the same share of the text width): a desktop
column sets the page's true geometry, a phone squeezes it in proportion.
Scene numbers hang in a gutter either side of the text. */
.prose[data-form='screenplay'] {
--sp-gutter: min(5ch, 6%);
--sp-text: calc(100% - 2 * var(--sp-gutter));
/* Full size wherever a column holds most of a script line; a phone sets
the type smaller rather than let every speech wrap word by word. */
font-size: clamp(0.8125rem, 0.55rem + 1.3vw, 1rem);
line-height: 1.5;
}
.prose[data-form='screenplay'] .chapter-title {
font-size: 1em;
font-weight: 700;
line-height: inherit;
letter-spacing: 0;
text-align: center;
text-transform: uppercase;
text-decoration: underline;
text-underline-offset: 0.2em;
margin: 0 0 1.5em;
}
.prose[data-form='screenplay'] .sp {
margin: 1.5em var(--sp-gutter) 0;
white-space: pre-line;
break-inside: avoid;
}
.prose[data-form='screenplay'] .sp-scene-heading {
position: relative;
font-size: 1em;
font-weight: 700;
line-height: inherit;
text-transform: uppercase;
margin-top: 3em;
break-after: avoid;
}
.prose[data-form='screenplay'] .chapter-title + .sp-scene-heading {
margin-top: 1.5em;
}
/* Scene numbers: the left one is ::after, because the studio's block
editor labels an open block with ::before — the right number yields to
that label while the block is being worked on. */
.prose[data-form='screenplay'] .sp-scene-heading[data-scene-number]::after,
.prose[data-form='screenplay'] .sp-scene-heading[data-scene-number]:not(.block-edit-open)::before {
content: attr(data-scene-number);
position: absolute;
top: 0;
width: var(--sp-gutter);
font-weight: 400;
color: var(--muted);
}
.prose[data-form='screenplay'] .sp-scene-heading[data-scene-number]::after {
right: 100%;
}
.prose[data-form='screenplay'] .sp-scene-heading[data-scene-number]:not(.block-edit-open)::before {
left: 100%;
text-align: right;
}
.prose[data-form='screenplay'] .sp-character {
margin-left: calc(var(--sp-gutter) + min(22ch, var(--sp-text) * 22 / 60));
text-transform: uppercase;
break-after: avoid;
}
.prose[data-form='screenplay'] .sp-parenthetical {
margin-top: 0;
margin-left: calc(var(--sp-gutter) + min(16ch, var(--sp-text) * 16 / 60));
max-width: 25ch;
/* A wrapped parenthetical hangs inside its opening bracket. */
padding-left: 1ch;
text-indent: -1ch;
break-after: avoid;
}
.prose[data-form='screenplay'] .sp-dialogue,
.prose[data-form='screenplay'] .sp-lyric {
margin-top: 0;
margin-left: calc(var(--sp-gutter) + min(10ch, var(--sp-text) * 10 / 60));
max-width: 35ch;
break-inside: auto;
}
.prose[data-form='screenplay'] .sp-lyric {
font-style: italic;
}
.prose[data-form='screenplay'] .sp-transition {
text-align: right;
text-transform: uppercase;
}
.prose[data-form='screenplay'] .sp-centered {
text-align: center;
}
/* A phone has no gutter to spare for a second number. */
@media (max-width: 36rem) {
.prose[data-form='screenplay'] .sp-scene-heading[data-scene-number]::before {
content: none;
}
}
/* In-book table of contents block. */
.toc ol,
.contents-list {
list-style: none;
margin: 1.5em 0;
padding: 0;
}
.toc li {
padding-left: calc(var(--toc-depth, 0) * 1.25em);
}
.toc-entry,
.contents-entry {
display: flex;
align-items: baseline;
gap: 0.6em;
padding: 0.42em 0;
color: var(--ink);
text-decoration: none;
}
.toc-entry:hover .toc-title,
.contents-entry:hover .contents-title {
color: var(--accent);
}
/* ——— Read-flow structure: covers and chapter pages ————————————— */
/* Cover page from the frontmatter document's coverImage blob. */
.doc-cover {
margin: 0;
}
.doc-cover img {
display: block;
max-width: 100%;
margin: 0 auto;
border-radius: 4px;
}
/* Paginated: the cover fills (at most) its own page, then breaks; each
chapter page starts at the top of a fresh column, like a book. */
.paginated-content .doc-cover {
break-after: column;
}
.paginated-content .doc-cover img {
max-height: calc(var(--page-h, 80vh) - 1rem);
}
.paginated-content .chapter + .chapter {
break-before: column;
}
.paginated-content .doc-cover + .chapter {
break-before: column;
}
/* Scroll: chapters flow, but a new one announces itself with air. */
.scroll-section .chapter + .chapter {
margin-top: 6rem;
}
.scroll-section .doc-cover {
margin: 2rem auto;
max-width: 24rem;
}
/* Chapter changed after the reader finished it (progress cid drift). */
.contents-updated {
font-family: var(--font-ui);
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--accent);
margin-left: 0.5em;
}
.follow-button {
display: inline-block;
margin-left: 0.75em;
}
.follow-error {
font-family: var(--font-ui);
font-size: 0.8rem;
color: color-mix(in srgb, #b23a3a 80%, var(--ink));
margin-left: 0.6em;
}
.toc-group,
.contents-group {
font-family: var(--font-ui);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--muted);
padding-top: 1.2em;
}
/* ——— Sky chrome (landing + shelf pages) ——————————————————— */
.air-label {
font-family: var(--font-ui);
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.sky {
position: relative;
display: flex;
flex-direction: column;
background: linear-gradient(
to bottom,
var(--sky-zenith) 0%,
var(--sky-mid) 34%,
var(--sky-horizon) 62%
);
color: var(--hero-ink);
overflow: hidden;
}
.sky-haze {
position: absolute;
inset: 0;
background:
radial-gradient(42% 18% at 18% 68%, rgba(255, 255, 255, 0.35), transparent 70%),
radial-gradient(55% 16% at 78% 55%, rgba(255, 255, 255, 0.28), transparent 70%),
radial-gradient(30% 12% at 45% 80%, rgba(255, 255, 255, 0.22), transparent 70%);
opacity: var(--haze-o);
animation: haze-drift 90s linear infinite alternate;
pointer-events: none;
}
@keyframes haze-drift {
from {
transform: translateX(-4%);
}
to {
transform: translateX(4%);
}
}
.sky-topbar {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.4rem clamp(1.25rem, 5vw, 4rem);
color: var(--hero-top-ink);
}
.sky-wordmark {
font-family: var(--font-ui);
font-weight: 600;
font-size: 1.05rem;
letter-spacing: 0.34em;
text-transform: uppercase;
color: inherit;
text-decoration: none;
}
.sky-topbar nav {
display: flex;
align-items: center;
gap: 1.6rem;
}
.sky-topbar nav a {
color: inherit;
text-decoration: none;
opacity: 0.85;
}
.sky-topbar nav a:hover {
opacity: 1;
text-decoration: underline;
text-underline-offset: 4px;
}
.theme-toggle {
font-size: 1.05rem;
line-height: 1;
color: inherit;
background: transparent;
border: 1px solid color-mix(in srgb, currentColor 45%, transparent);
border-radius: 999px;
width: 2.1rem;
height: 2.1rem;
cursor: pointer;
opacity: 0.85;
}
.theme-toggle:hover {
opacity: 1;
}
/* The horizon: a surveyed line with altitude ticks. */
.horizon {
position: relative;
flex-shrink: 0;
border-bottom: 2px solid var(--ink);
background: repeating-linear-gradient(
to right,
var(--ink) 0 1px,
transparent 1px 10.5rem
)
bottom / 100% 14px no-repeat;
height: 14px;
}
/* ——— Landing ——————————————————————————————————————————————— */
.landing {
min-height: 100dvh;
}
/* The flock, riding the open sky. White over the day gradient, a
moonlit silhouette at night (--flock-o follows the theme). */
.landing-flock {
position: absolute;
top: clamp(4.5rem, 14vh, 8rem);
right: clamp(1.5rem, 9vw, 8rem);
width: clamp(11rem, 26vw, 19rem);
aspect-ratio: 171.7 / 160;
color: var(--hero-top-ink);
opacity: var(--flock-o, 0.95);
pointer-events: none;
}
/* The bottom padding also keeps the last line clear of the menu flock
perched in the corner (.reader-nav-toggle). */
.landing-body {
position: relative;
margin: auto 0 0;
padding: 0 clamp(1.25rem, 5vw, 4rem) clamp(4.25rem, 7vh, 5rem);
max-width: 44rem;
}
.landing h1 {
font-family: var(--font-ui);
font-weight: 500;
font-size: clamp(2.2rem, 5.2vw, 3.4rem);
line-height: 1.08;
letter-spacing: 0.01em;
margin: 0 0 1.1rem;
text-wrap: balance;
}
/* The motto under the headline: free (as in speech) books. */
.landing-tagline {
opacity: 0.9;
margin: 0 0 2.2rem;
}
.air-btn {
font-family: var(--font-ui);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
border: 1px solid currentColor;
background: transparent;
color: inherit;
padding: 0.8rem 1.5rem;
cursor: pointer;
text-decoration: none;
transition:
background 0.15s,
color 0.15s;
}
.air-btn-solid {
background: var(--hero-ink);
color: var(--sky-horizon);
border-color: var(--hero-ink);
}
.air-btn-solid:hover {
background: transparent;
color: var(--hero-ink);
}
.air-btn-ghost:hover {
background: color-mix(in srgb, currentColor 10%, transparent);
}
.air-btn:disabled {
opacity: 0.6;
cursor: default;
}
.landing-error {
font-family: var(--font-ui);
font-size: 0.85rem;
margin: 1.2rem 0 0;
color: #a33b22;
}
:root[data-theme='dark'] .landing-error {
color: #ff9a80;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .landing-error {
color: #ff9a80;
}
}
.landing-signedin {
margin: 0 0 1.6rem;
font-family: var(--font-ui);
}
.landing-actions {
display: flex;
flex-wrap: wrap;
gap: 0.9rem;
}
/* ——— Shelf pages (browse / library) ——————————————————————— */
/* One continuous sky: zenith behind the wordmark, fading through the
horizon tone into paper partway down, with no dividing line — the
shelf sits in the open air. Gradient stops are in rem so the sky
band stays the same depth however long the shelf grows. */
.sky-page {
position: relative;
min-height: 100dvh;
color: var(--ink);
overflow-x: clip;
background: linear-gradient(
to bottom,
var(--sky-zenith) 0,
var(--sky-mid) 11rem,
var(--sky-horizon) 24rem,
var(--paper) 44rem
);
}
.sky-page > * {
position: relative;
}
.sky-page-haze {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 34rem;
background:
radial-gradient(45% 24% at 22% 55%, rgba(255, 255, 255, 0.35), transparent 72%),
radial-gradient(55% 26% at 76% 40%, rgba(255, 255, 255, 0.28), transparent 72%),
radial-gradient(35% 18% at 48% 72%, rgba(255, 255, 255, 0.22), transparent 72%);
opacity: var(--haze-o);
animation: haze-drift 90s linear infinite alternate;
pointer-events: none;
}
.sky-page .sky-topbar {
color: var(--hero-top-ink);
}
.sky-band-title {
color: var(--hero-ink);
position: relative;
padding: clamp(1.5rem, 5vh, 3rem) clamp(1.25rem, 5vw, 4rem) clamp(1.8rem, 5vh, 3rem);
}
.sky-band-title .air-label {
margin: 0 0 0.5rem;
opacity: 0.85;
}
.sky-band-title h1 {
font-family: var(--font-ui);
font-weight: 500;
font-size: clamp(1.9rem, 4.4vw, 2.8rem);
line-height: 1.05;
letter-spacing: 0.01em;
margin: 0;
}
.shelf-page {
max-width: 72rem;
margin: 0 auto;
padding: clamp(2.5rem, 7vh, 4.5rem) clamp(1.25rem, 5vw, 4rem);
}
.shelf-empty {
font-family: var(--font-ui);
color: var(--muted);
max-width: 34rem;
}
.shelf-opds {
font-family: var(--font-ui);
font-size: 0.875rem;
color: var(--muted);
margin-top: 3rem;
}
/* Covers in a dynamic column count: as many as fit, cards sharing the
row's width. */
.shelf-grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
gap: 2.4rem 1.6rem;
}
@media (min-width: 900px) {
.shelf-grid {
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
}
}
.shelf-book {
display: flex;
flex-direction: column;
}
.shelf-book-link {
display: flex;
flex-direction: column;
gap: 0.35rem;
color: inherit;
text-decoration: none;
}
.shelf-cover {
aspect-ratio: 2 / 3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0.9rem;
color: #f4f7fb;
margin-bottom: 0.5rem;
box-shadow: 0 8px 20px rgba(9, 18, 34, 0.22);
transition:
transform 0.16s ease,
box-shadow 0.16s ease;
}
.shelf-book:hover .shelf-cover {
transform: translateY(-4px);
box-shadow: 0 14px 28px rgba(9, 18, 34, 0.32);
}
.shelf-cover .air-label {
opacity: 0.8;
font-size: 0.62rem;
}
/* Real cover blobs fill the card edge to edge; the gradient variant
keeps its padded title. */
.shelf-cover.has-image {
padding: 0;
overflow: hidden;
background: var(--wash);
}
.shelf-cover.has-image img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.shelf-cover-title {
font-family: var(--font-ui);
font-weight: 500;
font-size: 1.05rem;
line-height: 1.2;
text-wrap: balance;
}
.shelf-book-title {
font-family: var(--font-ui);
font-weight: 500;
font-size: 0.95rem;
line-height: 1.3;
margin: 0;
}
.shelf-book-meta {
font-family: var(--font-ui);
font-size: 0.82rem;
line-height: 1.45;
color: var(--muted);
margin: 0;
}
.shelf-book-rating {
font-family: var(--font-ui);
font-size: 0.8rem;
color: var(--muted);
margin: 0.15rem 0 0;
display: flex;
align-items: baseline;
gap: 0.35rem;
}
/* ——— Stars (BookHive ratings) ————————————————————————————— */
/* Fractional display: a muted track with an accent overlay clipped to
the rating's width. */
.stars {
font-family: var(--font-ui);
line-height: 1;
}
.stars-track {
position: relative;
display: inline-block;
color: color-mix(in srgb, var(--muted) 45%, transparent);
letter-spacing: 0.08em;
}
.stars-fill {
position: absolute;
inset: 0 auto 0 0;
overflow: hidden;
white-space: nowrap;
color: var(--accent);
letter-spacing: 0.08em;
}
.rating-count {
color: var(--muted);
}
.star-input {
display: inline-flex;
gap: 0.1em;
}
.star-input-star {
font: inherit;
font-family: var(--font-ui);
background: none;
border: none;
padding: 0.1em;
cursor: pointer;
color: color-mix(in srgb, var(--muted) 70%, transparent);
line-height: 1;
}
.star-input-star.is-filled {
color: var(--accent);
}
.star-input-star:disabled {
opacity: 0.5;
cursor: default;
}
/* ——— Reading view ————————————————————————————————————————— */
/* The flock in the corner — the only chrome inside a book. Quiet
until wanted. */
.reader-nav-toggle {
position: fixed;
bottom: 1.1rem;
left: 1.1rem;
z-index: 40;
width: 2.4rem;
padding: 0;
border: none;
background: transparent;
color: var(--muted);
opacity: 0.4;
cursor: pointer;
transition: opacity 0.15s;
}
.reader-nav-toggle:hover,
.reader-nav-toggle:focus-visible,
.reader-nav-toggle[aria-expanded='true'] {
opacity: 0.9;
}
.reader-nav-toggle svg {
display: block;
width: 100%;
height: auto;
}
.reader-nav-backdrop {
position: fixed;
inset: 0;
z-index: 41;
background: color-mix(in srgb, var(--ink) 12%, transparent);
}
.reader-sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 42;
width: min(17rem, 85vw);
display: flex;
flex-direction: column;
gap: 2.2rem;
padding: 1.7rem 1.7rem 1.4rem;
background: var(--paper);
border-right: 1px solid var(--rule);
box-shadow: 12px 0 36px rgba(9, 18, 34, 0.14);
transform: translateX(-100%);
visibility: hidden;
transition:
transform 0.22s ease,
visibility 0.22s;
}
.reader-sidebar-open {
transform: none;
visibility: visible;
}
.reader-sidebar .sky-wordmark {
color: var(--ink);
}
.reader-sidebar-links {
display: flex;
flex-direction: column;
}
.reader-sidebar-app {
flex: 1;
}
.reader-sidebar-links a.mode-active {
color: var(--accent);
}
.reader-sidebar-links a,
.reader-sidebar-links button {
font-family: var(--font-ui);
font-size: 0.98rem;
color: var(--ink);
text-align: left;
text-decoration: none;
background: none;
border: none;
border-bottom: 1px solid var(--rule);
padding: 0.7rem 0.1rem;
cursor: pointer;
}
.reader-sidebar-links a:hover,
.reader-sidebar-links button:hover {
color: var(--accent);
}
/* Quieter tier above the foot: Cavu's own feedback links. */
.reader-sidebar-feedback {
margin-bottom: 1rem;
}
.reader-sidebar-feedback a {
font-size: 0.85rem;
color: var(--muted);
border-bottom: none;
padding: 0.25rem 0;
}
.reader-sidebar-foot {
display: flex;
align-items: center;
gap: 0.8rem;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.reader-sidebar {
transition: none;
}
}
@media (prefers-reduced-motion: reduce) {
.shelf-cover,
.shelf-book:hover .shelf-cover {
transition: none;
transform: none;
}
}
/* ——— Book overview ———————————————————————————————————————— */
.overview {
max-width: 46rem;
margin: 0 auto;
padding: 5.5rem 1.5rem 4rem;
}
.overview-hero {
display: flex;
align-items: flex-start;
gap: 2.75rem;
}
.overview-cover {
flex: 0 0 auto;
width: clamp(9rem, 26vw, 12.5rem);
height: auto;
border-radius: 8px;
box-shadow:
0 1px 2px color-mix(in srgb, var(--ink) 18%, transparent),
0 14px 34px color-mix(in srgb, var(--ink) 22%, transparent);
}
.overview-title {
min-width: 0;
}
.overview-eyebrow,
.overview-contents h2 {
font-family: var(--font-ui);
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--muted);
margin: 0 0 1rem;
}
.overview-title h1 {
font-family: var(--font-display);
font-weight: 560;
font-size: clamp(2.5rem, 7vw, 3.7rem);
line-height: 1.04;
letter-spacing: -0.015em;
margin: 0 0 1rem;
}
.overview-description {
font-size: 1.15rem;
font-style: italic;
color: color-mix(in srgb, var(--ink) 80%, var(--muted));
margin: 0 0 0.8rem;
}
.overview-meta {
font-family: var(--font-ui);
font-size: 0.85rem;
color: var(--muted);
margin: 0;
}
.overview-rating {
font-family: var(--font-ui);
font-size: 0.85rem;
margin: 0.5rem 0 0;
}
.overview-rating a {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
color: var(--muted);
text-decoration: none;
}
.overview-rating a:hover .rating-count {
text-decoration: underline;
}
.overview-rating-value {
color: var(--ink);
}
.overview-suggestions {
max-width: 34rem;
margin: 3.5rem auto 0;
}
.overview-suggestions h2 {
font-family: var(--font-ui);
font-size: 1rem;
font-weight: 500;
}
.suggestions-note {
font-family: var(--font-ui);
font-size: 0.9rem;
color: var(--muted);
line-height: 1.55;
}
.suggestions-posted {
font-family: var(--font-ui);
font-size: 0.9rem;
}
.suggestions-form {
display: grid;
gap: 0.6rem;
margin-top: 0.8rem;
font-family: var(--font-ui);
}
.suggestions-form-row {
display: flex;
align-items: center;
gap: 0.6rem;
}
.suggestions-form input,
.suggestions-form select,
.suggestions-form textarea {
font: inherit;
color: var(--ink);
background: transparent;
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.45rem 0.6rem;
}
.suggestions-form input {
flex: 1;
}
.suggestions-form textarea {
resize: vertical;
}
.suggestions-hint {
font-size: 0.78rem;
color: var(--muted);
margin-right: auto;
}
/* ——— Author page (pub.byline.profile over the Cavu shelf) ————— */
.author-hero {
display: flex;
align-items: center;
gap: 1.6rem;
}
.author-avatar {
width: 5.5rem;
height: 5.5rem;
border-radius: 50%;
object-fit: cover;
}
.author-page h1 {
font-family: var(--font-display);
font-weight: 560;
margin: 0;
}
.author-handle,
.author-pen-names,
.author-genres {
font-family: var(--font-ui);
font-size: 0.9rem;
color: var(--muted);
margin: 0.25rem 0 0;
}
.author-handle a {
color: inherit;
}
/* Bio and byline card copy arrive as markdown (components/Markdown). */
.author-bio {
max-width: 38rem;
margin: 1.6rem 0 0;
line-height: 1.6;
}
.author-bio p,
.author-bio ul,
.author-bio ol,
.author-bio blockquote {
margin: 0 0 0.8em;
}
.author-bio > :last-child {
margin-bottom: 0;
}
.author-book-description p,
.author-book-description ul,
.author-book-description ol {
margin: 0 0 0.35em;
}
.author-book-description > :last-child {
margin-bottom: 0;
}
.author-links {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-family: var(--font-ui);
font-size: 0.9rem;
margin: 1.2rem 0 0;
}
.author-books {
margin-top: 3rem;
}
.author-books h2 {
font-family: var(--font-ui);
font-size: 1rem;
font-weight: 500;
}
.author-book-list {
list-style: none;
margin: 1.2rem 0 0;
padding: 0;
display: grid;
gap: 1.4rem;
}
.author-book {
display: flex;
align-items: flex-start;
gap: 1rem;
color: var(--ink);
text-decoration: none;
}
.author-book:hover .author-book-title {
color: var(--accent);
}
.author-book-cover {
width: 3.6rem;
border-radius: 3px;
flex-shrink: 0;
}
.author-book-text {
display: grid;
gap: 0.25rem;
}
.author-book-title {
font-family: var(--font-display);
font-weight: 560;
}
.author-book-description {
font-family: var(--font-ui);
font-size: 0.88rem;
color: var(--muted);
}
.overview-eyebrow a {
color: inherit;
}
/* byline.pub discussion — same quiet column as suggestions/reviews. */
.overview-discussion {
max-width: 34rem;
margin: 3.5rem auto 0;
}
.overview-discussion h2 {
font-family: var(--font-ui);
font-size: 1rem;
font-weight: 500;
}
.comment-list,
.comment-replies {
list-style: none;
margin: 1.2rem 0 0;
padding: 0;
display: grid;
gap: 1.2rem;
}
.comment-replies {
margin: 0.9rem 0 0 1.4rem;
padding-left: 1rem;
border-left: 2px solid var(--rule);
}
.comment-meta {
font-family: var(--font-ui);
font-size: 0.85rem;
margin: 0 0 0.25rem;
}
.comment-date {
color: var(--muted);
}
.comment-text {
margin: 0;
font-size: 0.95rem;
line-height: 1.55;
white-space: pre-wrap;
}
.comment-actions {
display: flex;
gap: 0.5rem;
}
.comment-actions .btn-quiet {
font-size: 0.78rem;
padding: 0.1rem 0;
border: 0;
background: transparent;
color: var(--muted);
}
.comment-empty {
font-family: var(--font-ui);
font-size: 0.9rem;
color: var(--muted);
}
.comment-replying {
font-family: var(--font-ui);
font-size: 0.85rem;
color: var(--muted);
margin: 0;
}
.overview-reviews {
max-width: 34rem;
margin: 3.5rem auto 0;
}
.overview-reviews h2 {
font-family: var(--font-ui);
font-size: 1rem;
font-weight: 500;
}
.reviews-list {
list-style: none;
margin: 1.2rem 0 0;
padding: 0;
display: grid;
gap: 1.4rem;
}
.review-meta {
font-family: var(--font-ui);
font-size: 0.85rem;
margin: 0 0 0.3rem;
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.review-text {
margin: 0;
font-size: 0.95rem;
line-height: 1.55;
}
.overview-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.9rem;
margin-top: 2.2rem;
}
.overview-actions .follow-button,
.overview-actions .btn-quiet {
margin-left: 0;
}
/* "Download…" menu: EPUB / PDF, anchored under its button. */
.download-menu {
position: relative;
display: inline-block;
}
.download-menu-list {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 20;
min-width: 15rem;
margin: 0;
padding: 0.35rem;
list-style: none;
background: var(--paper);
border: 1px solid var(--rule);
border-radius: 8px;
box-shadow: 0 8px 24px color-mix(in srgb, var(--ink) 14%, transparent);
font-family: var(--font-ui);
}
.download-menu-list a {
display: grid;
gap: 0.1rem;
padding: 0.5rem 0.7rem;
border-radius: 6px;
color: var(--ink);
text-decoration: none;
}
.download-menu-list a:hover,
.download-menu-list a:focus-visible {
background: var(--wash);
}
.download-menu-format {
font-weight: 500;
}
.download-menu-hint {
font-size: 0.78rem;
color: var(--muted);
}
/* Offline save outcome (partial saves, errors) beside the button. */
.offline-note {
font-family: var(--font-ui);
font-size: 0.8rem;
color: var(--muted);
}
.bookhive-shelf {
display: inline-flex;
align-items: center;
gap: 0.6rem;
}
.bookhive-shelf .star-input {
font-size: 1.05rem;
}
.bookhive-status {
font: inherit;
font-family: var(--font-ui);
font-size: 0.85rem;
color: var(--accent);
background: transparent;
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.25rem 0.5rem;
}
.bookhive-review-toggle {
font-family: var(--font-ui);
font-size: 0.85rem;
color: var(--accent);
background: none;
border: none;
padding: 0.25rem 0;
cursor: pointer;
}
/* The editor takes its own full-width line under the actions row. */
.bookhive-shelf {
flex-wrap: wrap;
}
.bookhive-review {
/* flex-basis breaks the line; the width is set outright because the
inline-flex parent sizes to its first row's content. */
flex-basis: 100%;
width: min(34rem, 82vw);
display: grid;
gap: 0.5rem;
font-family: var(--font-ui);
}
.bookhive-review textarea {
font: inherit;
font-size: 0.9rem;
color: var(--ink);
background: transparent;
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.45rem 0.6rem;
resize: vertical;
}
.bookhive-review-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.begin-reading {
display: inline-block;
padding: 0.72em 1.7em;
background: var(--accent);
color: var(--paper);
font-family: var(--font-ui);
font-size: 0.92rem;
font-weight: 500;
text-decoration: none;
border-radius: 999px;
}
.begin-reading:hover {
background: color-mix(in srgb, var(--accent) 85%, var(--ink));
}
.overview-contents {
margin-top: 4.5rem;
}
.overview-footer {
margin-top: 5rem;
padding-top: 1.4rem;
border-top: 1px solid var(--rule);
font-family: var(--font-ui);
font-size: 0.78rem;
color: var(--muted);
}
.overview-footer code {
font-size: 0.95em;
word-break: break-all;
}
.load-error {
color: var(--muted);
font-style: italic;
}
/* ——— Docs ————————————————————————————————————————————————— */
.docs {
max-width: 42rem;
}
.docs-section {
margin-top: 3.6rem;
}
.docs-section h2 {
font-family: var(--font-display);
font-weight: 560;
font-size: 1.55rem;
line-height: 1.2;
letter-spacing: -0.01em;
margin: 0 0 0.9em;
}
.docs-section h3 {
margin: 1.8em 0 0.6em;
}
.docs-section ul,
.docs-section ol {
padding-left: 1.3em;
margin: 0 0 1.1em;
}
.docs-section li {
margin-bottom: 0.55em;
}
.docs-code {
font-size: 0.8rem;
line-height: 1.6;
background: var(--wash);
border: 1px solid var(--rule);
border-radius: 8px;
padding: 0.9em 1.1em;
overflow-x: auto;
margin: 1.4em 0;
}
/* Numbered resolution steps: big display numerals in the margin. */
.docs-steps {
list-style: none;
padding-left: 0 !important;
counter-reset: docs-step;
margin-top: 2.2em;
}
.docs-steps > li {
counter-increment: docs-step;
position: relative;
padding-left: 3.1rem;
margin-bottom: 2.4em;
}
.docs-steps > li::before {
content: counter(docs-step);
position: absolute;
left: 0;
top: -0.12em;
font-family: var(--font-display);
font-weight: 560;
font-size: 1.7rem;
color: color-mix(in srgb, var(--accent) 70%, var(--muted));
}
.docs-steps > li > h3 {
margin: 0 0 0.5em;
}
@media (max-width: 640px) {
.docs-steps > li {
padding-left: 2.4rem;
}
}
/* ——— Reader chrome ———————————————————————————————————————— */
.reader.mode-paginated,
.reader.mode-comic {
height: 100dvh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.reader-main {
flex: 1;
min-height: 0;
}
/* ——— Paginated mode ——————————————————————————————————————— */
/* Margin | page block | margin: the turn zones live in the side tracks,
so their arrows render in the true margins, never over text. */
.paginated {
position: relative;
height: 100%;
display: grid;
grid-template-columns: minmax(2.75rem, 1fr) minmax(0, 40.5rem) minmax(2.75rem, 1fr);
padding: 2.6rem 0 1.4rem;
overflow: hidden;
}
/* The strip: a sliding window of per-chunk column segments laid on one
continuous column grid (segment widths and the flex gap are set by
PaginatedReader so cross-segment column spacing matches the in-segment
gap — a two-up view can straddle a chunk edge). Turning translates the
strip; crossing a chunk edge is an ordinary turn. */
.paginated-strip {
height: 100%;
display: flex;
transition: transform 0.22s ease-out;
}
.paginated-content {
height: 100%;
flex: none;
column-fill: auto;
}
/* The turn: alongside the strip's slide, the page sways in depth like
paper being turned — hinged toward the side the new page comes from.
The a/b classes are identical twins; PaginatedReader alternates them
so back-to-back turns each restart the animation. */
.paginated-sway {
grid-column: 2;
grid-row: 1;
height: 100%;
min-width: 0;
/* Clip exactly at the page edge: only the current view's columns show,
never a sliver of the next page (drift from fractional column widths
stays behind this edge). */
overflow: hidden;
}
.paginated-sway.turn-forward.sway-a {
animation: paginated-sway-fwd-a 0.32s ease-out;
}
.paginated-sway.turn-forward.sway-b {
animation: paginated-sway-fwd-b 0.32s ease-out;
}
.paginated-sway.turn-back.sway-a {
animation: paginated-sway-back-a 0.32s ease-out;
}
.paginated-sway.turn-back.sway-b {
animation: paginated-sway-back-b 0.32s ease-out;
}
@keyframes paginated-sway-fwd-a {
from {
transform: perspective(2400px) rotateY(-1.6deg);
}
}
@keyframes paginated-sway-fwd-b {
from {
transform: perspective(2400px) rotateY(-1.6deg);
}
}
@keyframes paginated-sway-back-a {
from {
transform: perspective(2400px) rotateY(1.6deg);
}
}
@keyframes paginated-sway-back-b {
from {
transform: perspective(2400px) rotateY(1.6deg);
}
}
/* A chapter starting mid-record opens on a fresh page. Descendant, not
child: real books nest titles inside [data-doc] > section.chapter, and
:first-child still exempts a title already at its section's top (the
section break covers it — see .chapter + .chapter). */
.paginated-content .chapter-title:not(:first-child) {
break-before: column;
}
/* An author's explicit page break, in the one mode that has pages. */
.paginated-content .page-break {
break-before: column;
height: 0;
}
/* Continuous chapters run on after a rest instead of opening a page, so
both the forced break and the drop that assumes one are dropped. */
.paginated-content[data-chapter-break='continuous'] .chapter + .chapter,
.paginated-content[data-chapter-break='continuous'] .doc-cover + .chapter,
.paginated-content[data-chapter-break='continuous'] .chapter-title:not(:first-child) {
break-before: auto;
}
.paginated-content[data-chapter-break='continuous'] .chapter-title,
.scroll-section[data-chapter-break='continuous'] .chapter-title {
padding-top: 0;
margin-top: 2.6em;
}
/* Wide viewports read as facing pages: the container widens and
PaginatedReader splits each view into two columns (same breakpoint
as lib/twoUp). */
@media (min-width: 1200px) {
.paginated {
grid-template-columns: minmax(2.75rem, 1fr) minmax(0, 82.5rem) minmax(2.75rem, 1fr);
}
}
/* The chapter drop, as EPUB sets it: every chapter opens on a fresh page
with the title sunk half a page (print tradition; cf. Standard Ebooks'
sunk chapter headers). padding-top rather than margin-top — CSS
fragmentation truncates margins at forced column breaks, but padding
survives. --page-h is set by PaginatedReader on measure. */
.paginated-content .chapter-title {
padding-top: calc(var(--page-h, 100vh) * var(--chapter-drop, 0.5));
}
.page-zone {
grid-row: 1;
/* Almost full height: the flock keeps its corner. */
margin-bottom: 2.6rem;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
color: transparent;
font-size: 2.6rem;
font-family: var(--font-ui);
transition: color 0.15s ease-out;
}
/* A faint arrow surfaces while the cursor is in the turn area. */
.page-zone:hover:not(:disabled) {
color: color-mix(in srgb, var(--ink) 25%, transparent);
}
.page-zone:disabled {
cursor: default;
}
.page-zone-prev {
grid-column: 1;
}
.page-zone-prev::after {
content: '‹';
}
.page-zone-next {
grid-column: 3;
}
.page-zone-next::after {
content: '›';
}
/* ——— Comic spreads ———————————————————————————————————————— */
.comic {
position: relative;
height: 100%;
padding: 1.6rem 3.25rem 1.2rem;
overflow: hidden;
}
/* The comic isn't a grid, so the turn zones position absolutely:
full-height strips along each edge, riding over the art. */
.comic .page-zone {
position: absolute;
top: 0;
bottom: 0;
width: max(3.25rem, 12%);
}
.comic .page-zone-prev {
left: 0;
}
.comic .page-zone-next {
right: 0;
}
/* End-of-book overlay: revealed by turning forward on the final page,
floating above the page zones; backdrop click returns to the page. */
.comic-end {
position: absolute;
inset: 0;
z-index: 5;
display: grid;
place-items: center;
background: color-mix(in srgb, var(--paper) 55%, transparent);
}
.comic-end-card {
background: var(--paper);
border: 1px solid var(--rule);
border-radius: 10px;
padding: 1.4rem 2rem 1.1rem;
box-shadow: 0 12px 40px rgb(0 0 0 / 0.18);
max-width: min(26em, 85vw);
}
.comic-end-card .caught-up {
margin: 0;
}
.comic-spread {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.comic-slot {
display: flex;
align-items: center;
height: 100%;
flex: 0 1 50%;
min-width: 0;
}
/* Facing pages meet at the gutter. */
.comic-slot-left {
justify-content: flex-end;
}
.comic-slot-right {
justify-content: flex-start;
}
.comic-slot-center {
flex-basis: 100%;
justify-content: center;
}
.comic-page {
max-height: 100%;
max-width: 100%;
width: auto;
height: auto;
object-fit: contain;
background: #fff;
box-shadow: 0 1px 2px color-mix(in srgb, var(--ink) 18%, transparent),
0 6px 22px color-mix(in srgb, var(--ink) 12%, transparent);
}
/* The turn: the incoming page swings down flat like a leaf finishing its
turn, hinged on the edge nearest the side the page came from — forward
and back feel like opposite motions in both reading directions. */
.comic-ltr .turn-forward .comic-slot,
.comic-rtl .turn-back .comic-slot {
transform-origin: left center;
animation: comic-turn-from-right 0.34s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.comic-ltr .turn-back .comic-slot,
.comic-rtl .turn-forward .comic-slot {
transform-origin: right center;
animation: comic-turn-from-left 0.34s cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes comic-turn-from-right {
from {
opacity: 0.25;
transform: perspective(1400px) rotateY(-24deg);
filter: brightness(0.94);
}
to {
opacity: 1;
transform: perspective(1400px) rotateY(0deg);
filter: none;
}
}
@keyframes comic-turn-from-left {
from {
opacity: 0.25;
transform: perspective(1400px) rotateY(24deg);
filter: brightness(0.94);
}
to {
opacity: 1;
transform: perspective(1400px) rotateY(0deg);
filter: none;
}
}
/* Comic chunks reached in scroll mode: a plain vertical page column. */
.comic-column img {
display: block;
width: 100%;
height: auto;
margin-bottom: 0.35rem;
background: #fff;
}
/* ——— Scroll mode —————————————————————————————————————————— */
.scroll-reader {
max-width: 38rem;
margin: 0 auto;
padding: 3rem 1.5rem 7rem;
/* ScrollReader compensates layout shifts itself (see its scroll-anchor
logic); native scroll anchoring would apply the same correction a
second time, walking the position down on every image load. */
overflow-anchor: none;
}
.scroll-section {
scroll-margin-top: 72px;
}
/* Chapter openings carry the break, not record boundaries: chunk edges
stay invisible. The gap of open air mirrors the paginated chapter
drop — nothing is rendered before the title itself. */
.scroll-reader .chapter-title {
margin-top: calc(90vh * var(--chapter-drop, 0.5));
}
.section-loading {
text-align: center;
color: var(--muted);
letter-spacing: 0.5em;
padding: 2.5em 0;
}
.book-end {
text-align: center;
margin: 4em 0 2em;
}
/* ——— Highlights ——————————————————————————————————————————— */
/* Painted via the CSS Custom Highlight API — no elements involved.
Two axes on the reader's own marks: fill means "mine", an underline
means "other people can see it", and a solid rule means "there are
words here". Whether a note has a body is otherwise a popover
concern — encoding both axes in the fill would be unreadable. */
::highlight(cavu-mark),
::highlight(cavu-mark-note),
::highlight(cavu-mark-shared),
::highlight(cavu-mark-note-shared) {
background-color: color-mix(in srgb, var(--accent) 24%, transparent);
}
/* Mine, shared with a group. Still filled — it is my mark — but in the
group hue, because "some people can see this" is a different fact from
"everyone can", not a weaker version of it. */
::highlight(cavu-mark-group),
::highlight(cavu-mark-group-note) {
background-color: color-mix(in srgb, var(--group) 26%, transparent);
}
::highlight(cavu-mark-group-note) {
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--group) 80%, transparent);
text-decoration-thickness: 2px;
text-underline-offset: 0.18em;
}
/* Mine, with words, private. */
::highlight(cavu-mark-note) {
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--accent) 75%, transparent);
text-decoration-thickness: 2px;
text-underline-offset: 0.18em;
}
/* Mine, shared — the dotted rule is the same "on the network" idiom
other readers' marks use. */
::highlight(cavu-mark-shared) {
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: color-mix(in srgb, var(--accent) 75%, transparent);
text-decoration-thickness: 2px;
text-underline-offset: 0.18em;
}
/* Mine, with words, shared. */
::highlight(cavu-mark-note-shared) {
text-decoration: underline;
text-decoration-style: double;
text-decoration-color: color-mix(in srgb, var(--accent) 75%, transparent);
text-underline-offset: 0.18em;
}
/* Other readers' marks: a quiet dotted underline, no fill. */
::highlight(cavu-community) {
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: color-mix(in srgb, var(--accent) 65%, transparent);
text-underline-offset: 0.18em;
}
/* Other *members'* marks: the same quiet rule in the group hue, so a
passage the group has been round tells itself apart from one the
whole network has. */
::highlight(cavu-community-group) {
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: color-mix(in srgb, var(--group) 75%, transparent);
text-decoration-thickness: 2px;
text-underline-offset: 0.18em;
}
/* Placement word beside "Your note" — Private / Shared / Public. */
.highlight-mark-chip {
font-size: 0.75em;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
/* The "why" line under a hint the reader can't act on. */
.highlight-note-hint-sub {
display: block;
padding: 0 0.5rem 0.35rem;
font-size: 0.75rem;
color: var(--muted);
}
/* Community popover content. */
.highlight-mark-info {
display: grid;
gap: 0.3rem;
max-width: min(20rem, 75vw);
padding: 0.35rem 0.5rem;
font-size: 0.85rem;
}
.highlight-mark-meta {
margin: 0;
color: var(--muted);
}
.highlight-mark-meta strong {
color: var(--ink);
font-weight: 500;
}
.highlight-mark-body {
margin: 0;
line-height: 1.5;
}
.highlight-mark-info a,
.highlight-mark-info button {
padding: 0;
justify-self: start;
}
.highlight-mark-actions {
display: flex;
gap: 0.8rem;
}
/* Overlapping marks on one passage page through the popover. */
.highlight-mark-pager {
display: flex;
align-items: center;
align-self: stretch;
gap: 0.1rem;
margin-left: 0.2rem;
padding-left: 0.2rem;
border-left: 1px solid var(--rule);
color: var(--muted);
font-size: 0.78rem;
}
.highlight-mark-pager button {
padding: 0.35rem 0.45rem;
}
.highlight-mark-pager span {
white-space: nowrap;
}
/* ——— Feedback modal ———————————————————————————————————————— */
.feedback-modal {
width: min(32rem, 92vw);
max-height: min(80vh, 42rem);
overflow-y: auto;
}
.feedback-modal-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
}
.feedback-modal-title {
font-size: 1rem;
font-weight: 500;
}
.feedback-modal-tabs {
display: flex;
gap: 0.2rem;
padding: 0.15rem;
border: 1px solid var(--rule);
border-radius: 8px;
}
.feedback-modal-tabs button {
font: inherit;
font-size: 0.85rem;
color: var(--muted);
background: none;
border: none;
border-radius: 6px;
padding: 0.3rem 0.7rem;
cursor: pointer;
}
.feedback-modal-tabs button.is-active {
color: var(--ink);
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.feedback-modal-close {
font: inherit;
font-size: 1.1rem;
line-height: 1;
color: var(--muted);
background: none;
border: none;
border-radius: 6px;
padding: 0.2rem 0.5rem;
cursor: pointer;
}
.feedback-modal-close:hover {
color: var(--ink);
}
/* The suggestion boxes keep their page styling; only the page-flow
margins come off inside the modal. */
.feedback-modal .overview-suggestions {
max-width: none;
margin: 0.4rem 0 0;
}
/* ——— Share composer modal ————————————————————————————————— */
.share-modal-backdrop {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 1rem;
background: rgb(15 23 32 / 0.35);
}
.share-modal {
width: min(28rem, 92vw);
display: grid;
gap: 0.6rem;
padding: 1rem;
background: var(--paper);
border: 1px solid var(--rule);
border-radius: 10px;
box-shadow: 0 16px 48px rgb(0 0 0 / 0.25);
font-family: var(--font-ui);
}
.share-modal textarea {
font: inherit;
font-size: 0.95rem;
color: var(--ink);
background: transparent;
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.5rem 0.6rem;
resize: vertical;
}
.share-modal-meta {
display: flex;
justify-content: space-between;
gap: 0.8rem;
font-size: 0.8rem;
color: var(--muted);
}
.share-modal-card {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.share-modal-count.is-over {
color: #b3502f;
}
.share-modal-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
}
.share-modal-alt {
margin-right: auto;
font-size: 0.85rem;
color: var(--accent);
text-decoration: none;
}
.share-modal-alt:hover {
text-decoration: underline;
}
.share-modal-hint {
margin: 0;
font-size: 0.78rem;
color: var(--muted);
}
.share-modal-done {
margin: 0;
}
/* Floating toolbar above a selection or saved highlight. */
.highlight-toolbar {
position: fixed;
transform: translate(-50%, calc(-100% - 10px));
z-index: 30;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 0.1rem;
padding: 0.2rem;
max-width: calc(100vw - 16px);
background: var(--paper);
border: 1px solid var(--rule);
border-radius: 8px;
box-shadow: 0 6px 24px rgb(0 0 0 / 0.16);
font-family: var(--font-ui);
font-size: 0.85rem;
}
/* Touch: sits below the passage — the native selection callout and
end handle own the space above it. */
.highlight-toolbar.is-touch {
transform: translate(-50%, 14px);
}
.highlight-toolbar button,
.highlight-toolbar a {
font: inherit;
color: var(--accent);
background: none;
border: none;
border-radius: 6px;
padding: 0.35rem 0.65rem;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
touch-action: manipulation;
}
.highlight-toolbar button:hover,
.highlight-toolbar a:hover {
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.highlight-toolbar button:disabled {
opacity: 0.5;
cursor: default;
}
.highlight-toolbar-error {
color: #b3502f;
font-size: 0.78rem;
padding: 0 0.5rem;
max-width: 16em;
}
/* Note compose (margin.at) — the toolbar grows into a small card. */
.highlight-toolbar.is-composing {
padding: 0.5rem;
}
.highlight-note {
display: grid;
gap: 0.4rem;
width: min(22rem, 80vw);
}
.highlight-note textarea {
font: inherit;
color: var(--ink);
background: transparent;
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.45rem 0.55rem;
resize: vertical;
min-height: 3.5em;
}
.highlight-note-actions {
display: flex;
align-items: center;
gap: 0.1rem;
}
.highlight-note-hint {
color: var(--muted);
font-size: 0.75rem;
margin-right: auto;
padding-left: 0.2rem;
}
/* The caught-up seam: end of published content, book still ongoing. */
.caught-up {
text-align: center;
font-size: 1rem;
margin: 2.5em auto 0;
max-width: 26em;
}
.caught-up-note {
font-style: italic;
color: var(--muted);
margin-bottom: 1.2em;
}
.caught-up-hint {
font-family: var(--font-ui);
font-size: 0.8rem;
color: var(--muted);
margin-top: 1em;
}
/* ——— Small screens / motion ——————————————————————————————— */
@media (max-width: 640px) {
.overview {
padding: 3rem 1.25rem 3rem;
}
.overview-hero {
flex-direction: column;
gap: 1.6rem;
}
.overview-cover {
width: clamp(8rem, 38vw, 10.5rem);
}
.paginated {
padding: 2rem 1.4rem 1rem;
}
/* On a phone the gutters are slim, so the zones leave the grid and
become edge strips running to the very edge of the screen. Slim —
swiping is the primary turn gesture; wide strips sat over the text
and blocked starting a selection near the margins. The flock's
corner clearance carries over from the base margin. */
.page-zone {
grid-area: auto;
position: absolute;
top: 0;
bottom: 0;
width: max(2.75rem, 10vw);
}
.page-zone-prev {
left: 0;
}
.page-zone-next {
right: 0;
}
}
@media (max-width: 640px) {
.comic {
padding: 1rem 0.5rem 0.75rem;
}
}
@media (prefers-reduced-motion: reduce) {
.paginated-content {
transition: none;
}
.comic-spread .comic-slot,
.paginated-sway,
.sky-haze {
animation: none !important;
}
}
/* ——— Importer ————————————————————————————————————————————— */
.import {
max-width: 44rem;
margin: 0 auto;
padding: 3rem 1.4rem 6rem;
font-family: var(--font-ui);
}
.import h1 {
font-family: var(--font-display);
font-weight: 500;
margin: 0 0 0.5rem;
}
.import-lede {
color: var(--muted);
line-height: 1.5;
margin: 0 0 2rem;
}
.import-section {
border-top: 1px solid var(--rule);
padding: 1.5rem 0;
}
.import-section h2 {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 500;
margin: 0 0 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin: 0 0 0.9rem;
flex: 1;
}
.field > span {
font-size: 0.82rem;
color: var(--muted);
}
.field input,
.field select {
font: inherit;
color: var(--ink);
background: var(--wash);
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.5rem 0.65rem;
}
.field-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
/* Handle autocomplete (the sign-in combobox). The wrapper positions the
suggestion list; the input inside inherits .field input styling. */
.typeahead {
position: relative;
display: flex;
flex-direction: column;
}
.typeahead input {
width: 100%;
}
.typeahead-list {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 10;
margin: 0;
padding: 0.25rem;
list-style: none;
background: var(--paper);
border: 1px solid var(--rule);
border-radius: 8px;
box-shadow: 0 10px 24px rgba(9, 18, 34, 0.18);
max-height: 19rem;
overflow-y: auto;
}
.typeahead-option {
display: flex;
align-items: center;
gap: 0.55rem;
width: 100%;
font: inherit;
text-align: left;
color: var(--ink);
background: none;
border: none;
border-radius: 6px;
padding: 0.4rem 0.5rem;
cursor: pointer;
}
.typeahead-option[aria-selected='true'] {
background: var(--wash);
}
.typeahead-avatar {
width: 1.7rem;
height: 1.7rem;
border-radius: 50%;
background: var(--wash);
flex-shrink: 0;
}
.typeahead-handle {
font-family: var(--font-ui);
font-size: 0.92rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* The handle is what gets submitted — the display name yields first. */
flex-shrink: 0;
max-width: 100%;
}
.typeahead-name {
font-family: var(--font-ui);
font-size: 0.82rem;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn {
font: inherit;
color: var(--paper);
background: var(--accent);
border: none;
border-radius: 6px;
padding: 0.55rem 1.2rem;
cursor: pointer;
/* Links wearing this class must read as buttons too. */
display: inline-block;
text-decoration: none;
text-align: center;
}
.btn:disabled {
opacity: 0.5;
cursor: default;
}
.btn-quiet {
color: var(--accent);
background: transparent;
border: 1px solid var(--rule);
padding: 0.25rem 0.7rem;
margin-left: 0.5rem;
}
.import-hint {
font-size: 0.85rem;
color: var(--muted);
line-height: 1.5;
}
.import-error {
color: #b3502f;
}
.import-error-block {
white-space: pre-wrap;
font-size: 0.85rem;
}
.import-cover-row {
display: flex;
gap: 1.2rem;
align-items: flex-start;
}
.import-cover-preview {
max-height: 10rem;
border-radius: 4px;
border: 1px solid var(--rule);
}
.import-files {
list-style: none;
margin: 0.5rem 0;
padding: 0;
}
.import-files li {
padding: 0.45rem 0;
border-bottom: 1px dashed var(--rule);
font-size: 0.9rem;
}
.import-file-detail {
color: var(--muted);
}
.import-result {
background: var(--wash);
border: 1px solid var(--rule);
border-radius: 8px;
padding: 0.9rem 1.1rem;
margin-top: 1rem;
line-height: 1.6;
}
.import-signedin {
line-height: 1.6;
}
/* Granted-scope list on the account page — the "why won't this
integration button work" diagnostic. */
.login-scope {
font-family: var(--font-ui);
font-size: 0.82rem;
color: var(--muted);
margin-top: 0.6rem;
}
.login-scope summary {
cursor: pointer;
}
.login-scope ul {
margin: 0.4rem 0 0;
padding-left: 1.2rem;
display: grid;
gap: 0.15rem;
}
.login-scope code {
font-size: 0.78rem;
overflow-wrap: anywhere;
}
.login-actions {
display: flex;
gap: 0.75rem;
align-items: center;
margin: 1.2rem 0 0.8rem;
}
/* ——— Sign-in: one card over the sky (after cavu.games' Atmosphere
Login) — host buttons, Google via cavu.social, any other host by
handle, then a new cavu.social account. ——————————————————————— */
.login-sky {
min-height: 100dvh;
}
.login-panel-wrap {
position: relative;
padding: clamp(0.5rem, 5vh, 3.5rem) clamp(1rem, 5vw, 4rem) 5rem;
}
.login-panel {
max-width: 34rem;
margin: 0 auto;
font-family: var(--font-ui);
color: var(--ink);
background: var(--card);
border: 1px solid var(--rule);
box-shadow: 0 18px 50px rgba(23, 39, 60, 0.18);
padding: clamp(1.4rem, 5vw, 2.2rem);
}
.login-mark {
display: block;
width: 3rem;
aspect-ratio: 171.7 / 160;
color: var(--accent);
}
.login-panel h1 {
font-family: var(--font-display);
font-weight: 560;
font-size: clamp(1.6rem, 5vw, 1.85rem);
line-height: 1.15;
margin: 0.2rem 0 0.7rem;
}
.login-panel > p {
margin: 0 0 1rem;
font-size: 0.97rem;
line-height: 1.55;
}
.login-how {
margin: 0 0 1.3rem;
padding: 0.7rem 0.95rem;
/* --wash equals --card at night; an accent tint reads in both themes. */
background: color-mix(in srgb, var(--accent) 9%, var(--card));
font-size: 0.92rem;
line-height: 1.5;
}
.login-how summary {
cursor: pointer;
font-weight: 600;
}
.login-how p,
.login-how ul {
margin: 0.55rem 0 0;
}
.login-how ul {
padding-left: 1.2rem;
}
.login-panel .login-label {
display: block;
margin: 0 0 0.55rem;
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.login-hosts {
display: grid;
gap: 0.5rem;
}
/* Each host's own brand colour, as on their sign-in pages. */
.login-host {
min-height: 3rem;
border-radius: 999px;
border: 1.5px solid transparent;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.4rem 1rem;
font: inherit;
font-size: 0.97rem;
cursor: pointer;
transition: filter 0.15s;
}
.login-host:hover:not(:disabled) {
filter: brightness(1.08);
}
.login-host:disabled {
cursor: default;
opacity: 0.75;
}
.login-host span {
font-weight: 400;
opacity: 0.88;
}
.login-host strong {
font-weight: 600;
}
.login-host small {
margin-left: 0.3rem;
font-size: 0.8rem;
opacity: 0.7;
}
.login-host-bluesky {
background: #1185fe;
border-color: #0a6fd8;
color: #ffffff;
}
.login-host-northsky {
background: #7628bd;
border-color: #5f2099;
color: #ffffff;
}
.login-host-blacksky {
background: #111111;
border-color: #000000;
color: #ffffff;
}
.login-host-eurosky {
background: #dc6bb5;
border-color: #a83d84;
color: #111111;
}
/* Google's neutral sign-in button, light and dark. */
.login-host-google {
background: #ffffff;
border-color: #747775;
color: #1f1f1f;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .login-host-blacksky {
border-color: #4a5566;
}
:root:not([data-theme='light']) .login-host-google {
background: #131314;
border-color: #8e918f;
color: #e3e3e3;
}
}
:root[data-theme='dark'] .login-host-blacksky {
border-color: #4a5566;
}
:root[data-theme='dark'] .login-host-google {
background: #131314;
border-color: #8e918f;
color: #e3e3e3;
}
.login-host-mark {
width: 1.25rem;
height: 1.25rem;
flex: none;
margin-right: 0.15rem;
}
.login-or {
margin: 1.3rem 0 0;
padding-top: 1.1rem;
border-top: 1px solid var(--rule);
}
.login-input {
font: inherit;
font-size: 1.05rem;
color: var(--ink);
background: var(--paper);
border: 1.5px solid var(--rule);
border-radius: 2px;
padding: 0.7rem 0.85rem;
}
.login-input::placeholder {
color: color-mix(in srgb, var(--muted) 85%, transparent);
}
.login-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
/* On a phone the menu flock rides at the foot of the page (ReaderNav is
rendered inside .login-sky), below the card, not over its fields. */
@media (max-width: 40rem) {
.login-sky .reader-nav-toggle {
position: absolute;
}
}
.login-panel .login-field-hint {
margin: 0.45rem 0 0;
font-size: 0.85rem;
color: var(--muted);
}
.login-handle .air-btn {
width: 100%;
margin-top: 0.8rem;
}
.login-panel .login-create {
margin: 1.2rem 0 0;
font-size: 0.92rem;
color: var(--muted);
}
.login-create .btn-link {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 0.15em;
}
.login-panel .login-error {
margin: 1rem 0 0;
font-size: 0.88rem;
color: #a33b22;
}
:root[data-theme='dark'] .login-error {
color: #ff9a80;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .login-error {
color: #ff9a80;
}
}
.login-input {
width: 100%;
}
/* The confirmation step: who, and where the password will be typed.
The heading takes focus only so screen readers start there; it isn't
a control, so it shows no ring. */
.login-confirm h1:focus {
outline: none;
}
.login-confirm > p {
margin: 0 0 1rem;
line-height: 1.55;
overflow-wrap: anywhere;
}
.login-who {
display: flex;
align-items: center;
gap: 0.85rem;
margin: 0.4rem 0 1.2rem;
min-width: 0;
}
.login-who > div {
min-width: 0;
}
.login-who p {
margin: 0;
overflow-wrap: anywhere;
}
.login-avatar {
width: 3.25rem;
height: 3.25rem;
flex: none;
border-radius: 50%;
object-fit: cover;
display: grid;
place-items: center;
background: var(--accent);
color: var(--card);
font-weight: 600;
font-size: 1.3rem;
}
.login-who-name {
font-weight: 600;
font-size: 1.1rem;
}
.login-who-handle {
color: var(--muted);
}
.login-confirm .login-where {
padding: 0.75rem 0.95rem;
background: color-mix(in srgb, var(--accent) 9%, var(--card));
border-left: 3px solid var(--accent);
font-size: 1.02rem;
}
.login-confirm-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.9rem;
margin-top: 1.4rem;
}
.login-confirm-actions .air-btn {
width: 100%;
overflow-wrap: anywhere;
}
.login-confirm-actions .btn-link {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 0.15em;
font-size: 0.95rem;
}
@media (prefers-reduced-motion: reduce) {
.login-host {
transition: none;
}
}
/* ——— Account page: select-then-confirm ————————————————————————— */
/* Room for the fixed review bar, so the last section clears it. */
.login {
padding-bottom: 10rem;
}
.account-identity {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
line-height: 1.6;
}
.account-identity p {
margin: 0;
}
.account-identity .btn-quiet {
flex-shrink: 0;
margin-left: 0;
}
.account-outcome {
background: var(--wash);
border: 1px solid var(--rule);
border-radius: 8px;
padding: 0.9rem 1.1rem;
margin: 0 0 1.5rem;
line-height: 1.5;
}
.account-outcome p {
margin: 0 0 0.4rem;
}
.account-outcome p:last-child {
margin-bottom: 0;
}
.account-outcome-declined {
font-size: 0.88rem;
color: var(--muted);
}
/* Grant-tier picker. */
.account-choices {
display: flex;
flex-direction: column;
gap: 0.2rem;
margin: 1rem 0 0;
}
.account-choice {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.75rem;
align-items: start;
padding: 0.6rem 0.75rem;
margin: 0 -0.75rem;
border-radius: 6px;
cursor: pointer;
}
.account-choice input {
margin-top: 0.3rem;
accent-color: var(--accent);
}
.account-choice-pending {
background: var(--wash);
}
.account-choice-body {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.account-choice-title {
font-weight: 500;
}
.account-choice-detail,
.account-row-detail {
font-size: 0.85rem;
color: var(--muted);
line-height: 1.45;
}
.account-choice-note {
flex-shrink: 0;
font-size: 0.85rem;
color: var(--muted);
font-style: italic;
}
.account-choice-pending .account-choice-note {
color: var(--accent);
}
/* What the granted tier actually covers, itemised. */
.account-integrations {
margin: 1.1rem 0 0;
padding: 0.9rem 1.1rem;
border: 1px solid var(--rule);
border-radius: 8px;
background: var(--wash);
}
.account-integrations-head {
margin: 0 0 0.6rem;
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.account-integration {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.7rem;
align-items: start;
padding: 0.45rem 0;
}
.account-integration + .account-integration {
border-top: 1px solid var(--rule);
}
/* Granted reads as ink, missing as muted — the row itself says which. */
.account-integration > svg {
margin-top: 0.25rem;
color: var(--accent);
flex-shrink: 0;
}
.account-integration > svg circle {
color: var(--muted);
}
.account-integration-body {
display: flex;
flex-direction: column;
gap: 0.1rem;
min-width: 0;
}
.account-integration-name {
font-size: 0.95rem;
font-weight: 500;
}
.account-integration-visibility {
flex-shrink: 0;
font-size: 0.78rem;
color: var(--muted);
text-align: right;
max-width: 12rem;
}
.account-integrations-stale {
margin: 0.7rem 0 0;
padding-top: 0.7rem;
border-top: 1px solid var(--rule);
font-size: 0.85rem;
color: var(--muted);
line-height: 1.5;
}
.account-integrations-stale .btn-link {
color: var(--accent);
}
@media (max-width: 42rem) {
.account-integration {
grid-template-columns: auto 1fr;
}
.account-integration-visibility {
grid-column: 2;
text-align: left;
max-width: none;
}
}
/* Where-it-lives rows. */
.account-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.8rem 0.75rem;
margin: 0 -0.75rem;
border-bottom: 1px dashed var(--rule);
border-radius: 6px;
}
.account-row-pending {
background: var(--wash);
}
.account-row-title {
margin: 0;
font-weight: 500;
}
.account-row-detail {
margin: 0.1rem 0 0;
}
.account-row-pending-note {
margin: 0.2rem 0 0;
font-size: 0.85rem;
color: var(--accent);
font-style: italic;
}
.account-row-choices {
flex-shrink: 0;
display: flex;
gap: 1rem;
}
.account-radio {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.9rem;
cursor: pointer;
}
.account-radio input {
accent-color: var(--accent);
}
.account-row-static {
flex-shrink: 0;
font-size: 0.85rem;
color: var(--muted);
text-align: right;
max-width: 16rem;
}
/* Erase. */
.account-erase-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.7rem 0;
border-bottom: 1px dashed var(--rule);
}
.account-erase-row p {
margin: 0;
}
.btn-erase {
font: inherit;
font-size: 0.85rem;
color: #b3502f;
background: transparent;
border: 1px solid var(--rule);
border-radius: 6px;
padding: 0.2rem 0.6rem;
cursor: pointer;
}
.account-erase-confirm {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 0.6rem;
}
.account-erase-confirm .btn-link {
font-size: 0.85rem;
color: var(--muted);
}
.account-erase-busy {
font-size: 0.85rem;
color: var(--muted);
}
.account-erase-all {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-top: 1.2rem;
padding: 0.9rem 1.1rem;
border: 1px solid var(--rule);
border-radius: 8px;
background: var(--wash);
}
.account-erase-confirm-form {
flex-shrink: 0;
min-width: 18rem;
}
.account-erase-confirm-form .login-actions {
margin: 0;
}
.btn-danger {
background: #b3502f;
color: #f4f7fb;
}
/* The review bar: pinned, so a selection made anywhere stays actionable. */
.account-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 30;
background: var(--wash);
border-top: 1px solid var(--rule);
padding: 1rem 1.4rem;
box-shadow: 0 -6px 20px rgba(9, 18, 34, 0.08);
}
.account-bar-inner {
max-width: 44rem;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.25rem;
}
.account-bar-title {
margin: 0;
font-weight: 500;
}
.account-bar-detail {
margin: 0.15rem 0 0;
font-size: 0.85rem;
color: var(--muted);
line-height: 1.45;
}
.account-bar-actions {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.account-bar-actions .btn-quiet {
margin-left: 0;
}
@media (max-width: 42rem) {
.account-row,
.account-erase-row,
.account-erase-all,
.account-bar-inner {
flex-direction: column;
align-items: stretch;
}
.account-row-static {
text-align: left;
max-width: none;
}
}
/* KOReader sync panel on the account page. */
.koreader-sync {
background: var(--wash);
border: 1px solid var(--rule);
border-radius: 8px;
padding: 0.9rem 1.1rem;
margin-top: 1rem;
line-height: 1.6;
}
.koreader-sync h2 {
font-family: var(--font-ui);
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.4rem;
}
.koreader-sync .login-actions {
margin-bottom: 0;
}
.koreader-error {
font-family: var(--font-ui);
font-size: 0.88rem;
color: #a33a2a;
}
.koreader-creds {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.35rem 1rem;
margin: 0.8rem 0;
font-family: var(--font-ui);
font-size: 0.9rem;
}
.koreader-creds dt {
color: var(--muted);
}
.koreader-creds dd {
margin: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
overflow-wrap: anywhere;
}
.koreader-password {
font-size: 1rem;
letter-spacing: 0.04em;
}
.koreader-steps {
font-family: var(--font-ui);
font-size: 0.88rem;
color: var(--muted);
padding-left: 1.2rem;
margin: 0.4rem 0 0;
}
.btn-small {
padding: 0.15rem 0.6rem;
font-size: 0.8rem;
}
.login-actions .btn-quiet {
margin-left: 0;
}
/* Send to Kindle: account panel list + book-page modal. */
.kindle-list {
list-style: none;
margin: 0.6rem 0;
padding: 0;
display: grid;
gap: 0.35rem;
font-family: var(--font-ui);
font-size: 0.9rem;
}
.kindle-list li,
.kindle-pick label {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.6rem;
}
.kindle-list-name {
font-weight: 500;
}
.kindle-form .field-row {
margin-top: 0.4rem;
}
.kindle-form .login-actions {
margin-top: 0;
}
.kindle-pick {
display: grid;
gap: 0.45rem;
margin: 0;
padding: 0.6rem 0.75rem;
border: 1px solid var(--rule);
border-radius: 6px;
}
.kindle-pick label {
cursor: pointer;
}
.kindle-modal p {
margin: 0;
}
.kindle-modal .field {
margin-bottom: 0.5rem;
}
.btn-link {
background: none;
border: 0;
padding: 0;
font: inherit;
cursor: pointer;
}
/* ——— Studio ——————————————————————————————————————————————— */
.studio-book-list {
list-style: none;
margin: 1.6rem 0 0;
padding: 0;
}
.studio-book {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 1rem;
padding: 0.9rem 0.4rem;
border-top: 1px solid var(--rule);
color: var(--ink);
text-decoration: none;
}
.studio-book:hover {
background: var(--wash);
}
.studio-book-cover {
width: 3.2rem;
aspect-ratio: 2 / 3;
object-fit: cover;
border-radius: 3px;
flex-shrink: 0;
}
.studio-book-cover-blank {
display: block;
background: var(--wash);
border: 1px solid var(--rule);
}
.studio-book-text {
display: grid;
gap: 0.3rem;
}
.studio-book-title {
font-family: var(--font-display);
font-weight: 560;
display: flex;
align-items: baseline;
gap: 0.6rem;
flex-wrap: wrap;
}
.studio-book-meta {
font-family: var(--font-ui);
font-size: 0.85rem;
color: var(--muted);
}
.studio-badge {
font-family: var(--font-ui);
font-size: 0.68rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
border: 1px solid var(--rule);
border-radius: 999px;
padding: 0.15rem 0.6rem;
white-space: nowrap;
}
.studio-flag {
font-family: var(--font-ui);
font-size: 0.82rem;
color: var(--accent);
}
/* The offer to overwrite chapters whose uploaded text has drifted from
what's published, plus the documents an upload can't safely rewrite. */
.studio-update-offer {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
margin: 0 0 1rem;
}
.studio-update-offer .studio-flag {
margin: 0;
}
/* Spaces-support line under the studio lede; the probe result reads as
a quiet status, with the one-time re-check as an inline link. */
.studio-spaces {
font-family: var(--font-ui);
font-size: 0.88rem;
color: var(--muted);
margin: -1rem 0 2rem;
}
.studio-spaces strong {
color: inherit;
font-weight: 600;
}
.studio-spaces-yes strong {
color: var(--accent);
}
.studio-spaces .btn-link {
color: inherit;
text-decoration: underline;
text-underline-offset: 0.15em;
}
/* Reading-position privacy line on the account page: same quiet-status
treatment as .studio-spaces, private state tinted like a confirmation. */
.login-privacy {
font-family: var(--font-ui);
font-size: 0.88rem;
color: var(--muted);
margin: 0.35rem 0 0;
}
.login-privacy strong {
color: inherit;
font-weight: 600;
}
.login-privacy-on strong {
color: var(--accent);
}
.login-privacy .btn-link {
color: inherit;
text-decoration: underline;
text-underline-offset: 0.15em;
}
.studio-detail-header {
display: flex;
align-items: flex-start;
gap: 1.4rem;
margin: 1.6rem 0 1.8rem;
}
.studio-detail-header h1 {
display: flex;
align-items: baseline;
gap: 0.7rem;
flex-wrap: wrap;
margin-top: 0;
}
.studio-detail-cover {
width: 6.5rem;
border-radius: 4px;
flex-shrink: 0;
}
.studio-issues {
list-style: none;
margin: 0;
padding: 0.8rem 1rem;
border: 1px solid var(--rule);
background: var(--wash);
border-radius: 4px;
display: grid;
gap: 0.4rem;
font-family: var(--font-ui);
font-size: 0.88rem;
}
.studio-spine {
list-style: none;
margin: 0;
padding: 0;
counter-reset: studio-spine;
}
.studio-spine-doc {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
padding: 0.8rem 0;
border-top: 1px solid var(--rule);
counter-increment: studio-spine;
}
.studio-spine-doc:first-child {
border-top: none;
}
.studio-doc-text::before {
content: counter(studio-spine);
font-family: var(--font-ui);
font-size: 0.78rem;
color: var(--muted);
margin-right: 0.6rem;
}
.studio-doc-role {
font-family: var(--font-ui);
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.studio-doc-chapters {
list-style: none;
margin: 0.5rem 0 0;
padding-left: 1.4rem;
display: grid;
gap: 0.2rem;
font-family: var(--font-ui);
font-size: 0.85rem;
color: var(--muted);
}
/* Per-chapter visibility marker (spaces-capable authors only): a tiny
lock/unlock trailing the chapter title, tinted like the row text. */
.studio-chapter-access {
display: inline-flex;
vertical-align: -0.08em;
margin-left: 0.45rem;
color: var(--muted);
opacity: 0.85;
}
/* Reserved for M2's per-document actions; keeps rows from reflowing
when buttons arrive. */
.studio-doc-actions {
min-width: 6rem;
}
.studio-linked-record {
display: grid;
gap: 0.35rem;
}
.studio-linked-record p {
margin: 0;
}
.studio-linked-author {
display: flex;
align-items: center;
gap: 0.7rem;
}
.studio-linked-avatar {
width: 2.4rem;
height: 2.4rem;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
/* Release panel: plan summary, preview, actions. */
.studio-plan {
list-style: none;
margin: 0 0 1rem;
padding: 0.8rem 1rem;
border-left: 3px solid var(--accent);
background: var(--wash);
display: grid;
gap: 0.35rem;
font-family: var(--font-ui);
font-size: 0.9rem;
}
.studio-plan-warnings {
list-style: none;
margin: 0 0 1rem;
padding: 0;
display: grid;
gap: 0.25rem;
}
.studio-preview {
border: 1px solid var(--rule);
border-radius: 4px;
margin: 0 0 0.8rem;
}
.studio-preview summary {
cursor: pointer;
padding: 0.6rem 0.9rem;
font-family: var(--font-ui);
font-size: 0.9rem;
}
.studio-preview-body {
max-height: 24rem;
overflow-y: auto;
padding: 0 1.4rem 1.4rem;
border-top: 1px solid var(--rule);
}
/* --- The preview as a block editor (components/BlockEdit) ---
The chrome lives inside the block it belongs to: any wrapper element
would break the `hr + p` / `.chapter-title + p` sibling rules the prose
above depends on. So the marker is an outline (which draws no box) and
the pencil is an absolutely positioned child. */
/* Editing wants more of the chapter in view than reviewing does, but a
long release still has to stay a panel rather than a page. The body's
1.4rem side padding is what keeps the outlines and the pencil clear of
the scroller's edge. */
.studio-preview-editable .studio-preview-body {
max-height: 42rem;
padding-top: 0.9rem;
}
.block-edit {
position: relative;
outline: 1px dotted color-mix(in srgb, var(--rule) 60%, transparent);
outline-offset: 4px;
}
.block-edit:hover,
.block-edit-open {
outline-color: var(--accent);
outline-style: dashed;
}
/* The block's name, shown only while it's being worked on — a rule and a
page break look alike otherwise. */
.block-edit-open::before {
content: attr(data-block-label);
position: absolute;
top: -1.55em;
left: 0;
font-family: var(--font-ui);
font-size: 0.68rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent);
text-indent: 0;
pointer-events: none;
}
/* A rule and a page break can't hold a child, so they take the one
wrapper in the design; it behaves as the mark it wraps. */
.block-edit-void {
margin: 0;
}
.block-edit-void .page-break {
outline: 1px dashed color-mix(in srgb, var(--accent) 45%, transparent);
outline-offset: -2px;
}
.block-edit-chrome {
position: absolute;
top: -0.35em;
right: -0.35em;
text-indent: 0;
font-style: normal;
line-height: 0;
}
.block-edit-pencil {
display: grid;
place-items: center;
width: 1.55rem;
height: 1.55rem;
padding: 0;
border: 1px solid var(--rule);
border-radius: 50%;
background: var(--paper);
color: var(--muted);
cursor: pointer;
opacity: 0;
transition: opacity 120ms ease;
}
.block-edit:hover > .block-edit-chrome > .block-edit-pencil,
.block-edit-pencil:focus-visible,
.block-edit-open > .block-edit-chrome > .block-edit-pencil {
opacity: 1;
}
.block-edit-busy {
opacity: 1;
color: var(--accent);
border-color: var(--accent);
cursor: progress;
}
.block-edit-spinner {
animation: block-edit-spin 0.8s linear infinite;
transform-origin: 8px 8px;
}
@keyframes block-edit-spin {
to {
transform: rotate(360deg);
}
}
.block-edit-pencil:hover,
.block-edit-open > .block-edit-chrome > .block-edit-pencil {
color: var(--accent);
border-color: var(--accent);
}
/* Fixed, not absolute: the preview body scrolls in the release panel and
would clip a menu positioned inside it. BlockMenu measures the pencil
and closes on scroll rather than chasing it. */
.block-edit-menu {
position: fixed;
z-index: 60;
width: 13.5rem;
padding: 0.45rem;
border: 1px solid var(--rule);
border-radius: 8px;
background: var(--paper);
box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
font-family: var(--font-ui);
font-style: normal;
/* The prose around it sets a reading line-height, and the chrome sets
none at all; the menu is UI and wants its own. */
line-height: 1.4;
text-indent: 0;
text-align: left;
}
.block-edit-note {
margin: 0.35rem 0.45rem 0.5rem;
font-size: 0.78rem;
line-height: 1.35;
color: var(--muted);
}
.block-edit-menu-head:first-child {
margin-top: 0.3rem;
}
.block-edit-menu-head {
margin: 0.55rem 0.45rem 0.25rem;
font-size: 0.68rem;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--muted);
}
.block-edit-kinds {
list-style: none;
margin: 0 0 0.15rem;
padding: 0;
}
.block-edit-kind {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
padding: 0.3rem 0.45rem;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--ink);
font: inherit;
font-size: 0.85rem;
text-align: left;
cursor: pointer;
}
.block-edit-kind:hover {
background: var(--wash);
}
.block-edit-kind[aria-pressed='true'] {
color: var(--accent);
font-weight: 600;
}
.block-edit-kind[aria-pressed='true']::after {
content: '✓';
font-weight: 400;
}
.block-edit-hint {
font-size: 0.72rem;
color: var(--muted);
}
.block-edit-remove {
margin-top: 0.15rem;
border-top: 1px solid var(--rule);
border-radius: 0 0 5px 5px;
color: var(--muted);
}
.studio-release-actions {
display: flex;
gap: 0.75rem;
align-items: center;
margin-top: 0.6rem;
}
/* Downloads & print settings form. */
.studio-settings-form {
display: grid;
gap: 0.6rem;
max-width: 24rem;
}
.studio-checkbox {
display: flex;
align-items: center;
gap: 0.6rem;
font-family: var(--font-ui);
font-size: 0.95rem;
}
.studio-checkbox input {
width: 1.05rem;
height: 1.05rem;
accent-color: var(--accent);
}
.studio-action-btn {
font-size: 0.8rem;
padding: 0.3rem 0.8rem;
}
/* --- studio: index title row + first-publish wizard --- */
.studio-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.studio-title-row h1 {
margin-bottom: 0;
}
.studio-breadcrumb {
margin: 1.2rem 0 -0.6rem;
font-size: 0.85rem;
color: var(--muted);
}
.studio-wizard {
border: 0;
padding: 0;
margin: 0;
min-width: 0;
}
.studio-wizard:disabled {
opacity: 0.6;
}
.import-files li .studio-action-btn {
margin-left: 0.4rem;
}
.studio-strategy {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.35rem;
font-size: 0.85rem;
color: var(--muted);
}
.studio-strategy select {
font: inherit;
max-width: 100%;
}
/* --- studio: per-chapter selection in the file list --- */
.studio-chapter-picker {
margin-top: 0.35rem;
}
.studio-chapter-picker-tools {
display: flex;
gap: 0.4rem;
margin: 0.2rem 0 0.2rem 1.4rem;
}
.studio-chapter-picker .studio-doc-chapters {
margin-top: 0.2rem;
}
.studio-chapter-picker .studio-checkbox {
font-size: 0.85rem;
color: var(--ink);
}
.studio-chapter-picker .studio-checkbox input {
width: 0.9rem;
height: 0.9rem;
}
.studio-chapter-published span:first-child {
color: var(--muted);
}
/* ————————————————————————————————————————————————————————————
Group reads. The list pages borrow the shelf's chrome; what's new is
the progress board and the invite panel.
———————————————————————————————————————————————————————————— */
.group-page h1 {
font-family: var(--font-display);
margin: 0 0 0.2rem;
}
.group-head {
margin-bottom: 2rem;
}
.group-book {
color: var(--muted);
font-family: var(--font-ui);
margin: 0.2rem 0 0.8rem;
}
.group-list {
list-style: none;
margin: 0 0 1rem;
padding: 0;
}
.group-list li {
align-items: baseline;
border-bottom: 1px solid var(--rule);
display: flex;
gap: 0.75rem;
justify-content: space-between;
padding: 0.55rem 0;
}
.group-list a {
font-family: var(--font-ui);
font-size: 1.02rem;
}
/* The board. A bar per member, sorted by nothing in particular — this is
company, not a leaderboard, so no ranking and no ordering by progress. */
.group-board,
.group-roster {
list-style: none;
margin: 0;
padding: 0;
}
.group-board li,
.group-roster li {
align-items: center;
border-bottom: 1px solid var(--rule);
display: flex;
gap: 0.75rem;
justify-content: space-between;
padding: 0.5rem 0;
}
.group-member {
align-items: center;
display: flex;
font-family: var(--font-ui);
gap: 0.4rem;
}
.group-progress {
align-items: center;
color: var(--muted);
display: flex;
font-family: var(--font-ui);
font-size: 0.86rem;
gap: 0.5rem;
}
.group-progress-bar {
background: var(--wash);
border-radius: 999px;
display: block;
height: 6px;
overflow: hidden;
width: 7rem;
}
.group-progress-bar > span {
background: var(--group);
display: block;
height: 100%;
}
.group-progress-empty {
color: var(--muted);
font-family: var(--font-ui);
font-size: 0.86rem;
font-style: italic;
}
.group-invite {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin: 0.4rem 0 0.6rem;
}
.group-invite code {
background: var(--wash);
border-radius: 6px;
font-size: 1.1rem;
letter-spacing: 0.08em;
padding: 0.35rem 0.6rem;
}
/* The book page's block, sized to disappear when the reader has no
groups — it must not grow a whole empty region for a feature most
readers will never switch on. */
.overview-groups {
margin: 2rem 0;
}
.overview-groups h2 {
font-family: var(--font-ui);
font-size: 0.82rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
/* The multi-group share picker in the mark toolbar. */
.highlight-group-share {
align-items: center;
display: inline-flex;
gap: 0.3rem;
}
.highlight-group-share select {
font-family: var(--font-ui);
font-size: 0.82rem;
max-width: 11rem;
}
/* ——— Email notifications ————————————————————————————————— */
/* The account panel and the two token landing pages share the channel
list; it is the same two switches wherever it appears. */
.notify-channels {
list-style: none;
margin: 0.6rem 0 0.9rem;
padding: 0;
display: grid;
gap: 0.4rem;
font-family: var(--font-ui);
font-size: 0.9rem;
}
.notify-channels label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.notify-form {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 0.6rem;
margin-top: 0.6rem;
}
.notify-form .field {
flex: 1 1 16rem;
}
/* The per-book opt-in. It sits inside .overview-actions next to Follow
and inside .caught-up at the end of a chapter, so it claims its own
line in both rather than competing with the button beside it. */
.notify-inline {
flex-basis: 100%;
margin-top: 0.6rem;
font-family: var(--font-ui);
font-size: 0.85rem;
}
.caught-up .notify-inline {
margin-top: 1.2rem;
}
/* Deliberately not a .btn: before the reader has said yes this is an
offer, and an offer that looks like the Follow button beside it reads
as a second, competing decision. */
.notify-inline-open {
background: none;
border: 0;
padding: 0;
font: inherit;
color: var(--muted);
text-decoration: underline;
text-underline-offset: 0.2em;
cursor: pointer;
}
.notify-inline-open:hover {
color: var(--ink);
}
.notify-inline-note {
color: var(--muted);
margin: 0 0 0.5rem;
}
.notify-inline-note code,
.notify-panel code {
font-size: 0.95em;
}
.notify-inline-also {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.6rem;
color: var(--muted);
cursor: pointer;
}
.notify-inline .notify-form {
margin-top: 0;
}
/* Follow, on the author page. */
.author-actions {
margin: 0.6rem 0 0;
}
.author-actions .follow-button {
margin-left: 0;
}
.follow-hint {
font-family: var(--font-ui);
font-size: 0.8rem;
color: var(--muted);
margin-left: 0.6em;
}