Split CSS into base and theme, link them separately
- Move shared structural styles to frontend/src/assets/pagerite.css with conservative variables, and purple theme overrides to frontend/src/assets/themes/purple/theme.css. - Backend now links the base and theme stylesheets as separate files so they can be swapped or augmented. - Add blocking="render" to stylesheet links to avoid FOUC. - Drop redundant defer from module script tags. - Update related docs and comments.
This commit is contained in:
@@ -0,0 +1,678 @@
|
||||
/* Base structural and functional styles for Pagerite pages and editors.
|
||||
Color choices are intentionally conservative CSS variables so themes can
|
||||
override them without fighting layout or interaction rules. */
|
||||
@import url("./fonts/fonts.css");
|
||||
@import url("./pygments.css");
|
||||
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--surface: #f5f5f5;
|
||||
--text: #1a1a1a;
|
||||
--muted: #666666;
|
||||
--accent: #0056b3;
|
||||
--accent2: #003d80;
|
||||
--line: #0000001a;
|
||||
/* Width of the docked editor panel (used both here for shifting the page
|
||||
and in the Vue editor's own styles). */
|
||||
--editor-w: min(46rem, 50vw);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Literata", Georgia, serif;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.65;
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
/* Full-bleed elements (.wide) size to 100vw, which counts the vertical
|
||||
scrollbar; clip the few stray pixels instead of scrolling. */
|
||||
overflow-x: clip;
|
||||
/* Full height even on short pages: the footer sits at the bottom and the
|
||||
docked editor (sized by #content) never collapses. */
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Full-width banner: image header with the brand and nav overlaid. */
|
||||
#banner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
min-height: 11rem;
|
||||
background: url("./banner.svg") center 40% / cover;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* Per-page banner content (img, styled div, canvas...) overlays the
|
||||
default artwork; swapped along with #nav/#main on fetch-navigation. */
|
||||
#page-banner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#page-banner>* {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#brand,
|
||||
#nav {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-family: "Fraunces", serif;
|
||||
font-weight: 700;
|
||||
font-size: 2.4rem;
|
||||
text-decoration: none;
|
||||
margin: auto 1.25rem 0;
|
||||
padding-top: 1.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Nav overlaid at the bottom of the banner */
|
||||
#nav {
|
||||
font-size: 1.3em;
|
||||
text-shadow: 0 0 0.1em black;
|
||||
padding: 0.35rem 1.25rem;
|
||||
}
|
||||
|
||||
#nav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 0.25rem 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#nav a {
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#nav ul ul a {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#nav a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#nav span {
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
#nav .current {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Sidebar + main row. A symmetric grid: the article column is sized by the
|
||||
viewport alone (never by content), with equally sized flexible gutters
|
||||
on both sides. The sidebar sits in the left gutter, so it appearing or
|
||||
disappearing never shifts the article; the right gutter balances it. */
|
||||
#content {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(0, 78rem) 1fr;
|
||||
/* The docked editor pushes the content (not the header) right. */
|
||||
transition: margin-left 0.25s ease;
|
||||
}
|
||||
|
||||
body.editing #content {
|
||||
margin-left: var(--editor-w);
|
||||
padding-left: 1rem;
|
||||
/* gap between the docked editor and the content */
|
||||
/* No overflow clipping here: .editor-host lives outside this box
|
||||
(negative left), and .wide shrink-wraps to the remaining space. */
|
||||
}
|
||||
|
||||
/* The sidebar's gutter space is needed by the editor instead. */
|
||||
body.editing #sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The editor host lives inside #content: it starts below the banner and
|
||||
ends above the footer. The panel itself sticks to the viewport while
|
||||
scrolling (but never taller than the content area). */
|
||||
.editor-host {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: calc(0px - var(--editor-w));
|
||||
width: var(--editor-w);
|
||||
}
|
||||
|
||||
.editor-root.overlay {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
max-height: 100%;
|
||||
background: var(--bg);
|
||||
animation: editor-slide-in 0.25s ease;
|
||||
}
|
||||
|
||||
.editor-root.overlay.closing {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes editor-slide-in {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* The banner's own pen: opens the site editor (banner + structure).
|
||||
Qualified with `button` to beat the later .edit-link rule's left offset
|
||||
(both classes apply to the same element). */
|
||||
button.banner-edit-link {
|
||||
position: absolute;
|
||||
top: 0.6rem;
|
||||
right: 1.25rem;
|
||||
left: auto;
|
||||
z-index: 10;
|
||||
font: inherit;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
grid-column: 1;
|
||||
/* Pinned to the page's left edge (not the article's) and kept in view
|
||||
while scrolling. Translucent + blurred rather than an opaque box, so
|
||||
full-bleed .wide images can pass underneath without a hard edge. */
|
||||
justify-self: start;
|
||||
align-self: start;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
width: 12rem;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1rem 1rem 1.25rem;
|
||||
border-radius: 0 0 0.5rem 0;
|
||||
background: color-mix(in srgb, var(--bg) 75%, transparent);
|
||||
backdrop-filter: blur(0.5rem);
|
||||
}
|
||||
|
||||
#sidebar:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8em;
|
||||
line-height: 1.0;
|
||||
}
|
||||
|
||||
#sidebar a {
|
||||
text-decoration: none;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#sidebar a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#sidebar .current {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-column: 2;
|
||||
/* No top padding: a leading wide image sits flush under the banner, and
|
||||
text-first pages get their spacing from the h1's top margin instead. */
|
||||
padding: 0 1.25rem 3rem;
|
||||
}
|
||||
|
||||
article h1,
|
||||
article h2,
|
||||
article h3 {
|
||||
font-family: "Fraunces", serif;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
font-size: 2.2rem;
|
||||
margin: 2rem 0 1.2rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Margin strategy: bottom-only inside articles. Top margins misalign
|
||||
column tops and collapse unpredictably; spacing comes from below. */
|
||||
article p,
|
||||
article ul,
|
||||
article ol,
|
||||
article dl,
|
||||
article blockquote,
|
||||
article pre,
|
||||
article figure {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
margin: 1.4rem 0 0.4rem;
|
||||
color: color-mix(in oklab, var(--accent2) 60%, var(--muted));
|
||||
}
|
||||
|
||||
/* Lists: small diamond emoji markers — blue 🔹 on odd nesting levels,
|
||||
orange 🔸 on even. The marker occupies a 1em outdented box so wrapped
|
||||
lines align. */
|
||||
article ul {
|
||||
list-style: none;
|
||||
padding-inline-start: 1em;
|
||||
}
|
||||
|
||||
article ul li::before {
|
||||
content: "🔹";
|
||||
display: inline-block;
|
||||
margin-left: -1.3em;
|
||||
width: 1.3em;
|
||||
}
|
||||
|
||||
article ul ul li::before {
|
||||
content: "🔸";
|
||||
}
|
||||
|
||||
article ul ul ul li::before {
|
||||
content: "🔹";
|
||||
}
|
||||
|
||||
/* Task lists: real checkboxes (not emoji), clickable in the public page. */
|
||||
article .task-list-item::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
article .task-list-item-checkbox {
|
||||
cursor: pointer;
|
||||
margin-inline-end: 0.35em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
article {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
/* In the left gutter, on the same side as the docked editor panel. */
|
||||
left: -2.2rem;
|
||||
z-index: 2;
|
||||
/* stay above full-bleed .wide images */
|
||||
font: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
text-shadow: 0 0 0.1em black;
|
||||
}
|
||||
|
||||
/* pagerite.js tucks the pen at the end of the article's first h1. */
|
||||
article h1 .edit-link {
|
||||
position: static;
|
||||
font-size: 1.1rem;
|
||||
vertical-align: 0.3em;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.edit-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
article p,
|
||||
article li,
|
||||
article dd {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
/* Multi-column reading on wide displays, but only for long articles
|
||||
(pagerite.js adds .multicol based on content length and splits the body
|
||||
into .colseg segments separated by full-width h2s and wide figures;
|
||||
only segments with enough text get .cols and thus columns). Columns only
|
||||
reflow text inside the article; the article's width never changes. */
|
||||
@media (min-width: 100rem) {
|
||||
.multicol .colseg.cols {
|
||||
columns: 2;
|
||||
column-gap: 3.5rem;
|
||||
column-rule: 1px solid var(--line);
|
||||
}
|
||||
}
|
||||
|
||||
.multicol .colseg {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
p,
|
||||
li {
|
||||
break-inside: avoid-column;
|
||||
}
|
||||
|
||||
figure,
|
||||
pre,
|
||||
blockquote,
|
||||
table,
|
||||
dl {
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
article h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 2.2rem 0 0.6rem;
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
article a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
article a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Blockquotes: inner paragraphs carry no margins (spacing comes from the
|
||||
blockquote itself, bottom-only like everything else in articles). The
|
||||
negative left margin pushes the bar out past the text edge, so quoted
|
||||
text aligns with the surrounding paragraphs — same trick as code blocks. */
|
||||
blockquote {
|
||||
margin: 0 0 1rem -0.5rem;
|
||||
padding: 0 0 0 0.25rem;
|
||||
border-left: 0.25rem solid var(--accent2);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
padding: 0.5rem 0.8rem;
|
||||
/* Code text aligns with the surrounding paragraphs: the box extends
|
||||
past them by its own padding. */
|
||||
margin-left: -0.8rem;
|
||||
margin-right: -0.8rem;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Inline code integrates with the text, no box of its own */
|
||||
p code,
|
||||
li code {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Click-to-copy button (added by pagerite.js) */
|
||||
.copy {
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
right: 0.35rem;
|
||||
font: inherit;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.6rem;
|
||||
color: var(--muted);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
pre:hover .copy,
|
||||
.copy:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy.copied {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Fira Code", ui-monospace, monospace;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 0.35rem 0.8rem;
|
||||
}
|
||||
|
||||
/* Images and figures */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
/* Captions of full-bleed images: centered and kept to a readable width. */
|
||||
figure:has(.wide) figcaption {
|
||||
max-width: 65ch;
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Positioning via brace-attribute classes: {.right}, {.left}, {.wide} */
|
||||
figure:has(.right),
|
||||
img.right {
|
||||
float: right;
|
||||
margin: 0.3rem 0 1rem 1.5rem;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
figure:has(.left),
|
||||
img.left {
|
||||
float: left;
|
||||
margin: 0.3rem 1.5rem 1rem 0;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
/* .wide is full bleed: edge to edge of the viewport (or of the space left
|
||||
of the docked editor). Centered on the article column — which is itself
|
||||
centered in the available space — via margin-left: 50% + translateX(-50%).
|
||||
The sidebar stacks above it (z-index + opaque background). */
|
||||
figure:has(.wide),
|
||||
img.wide {
|
||||
display: block;
|
||||
/* no inline strut/descender gaps around the image */
|
||||
width: 100vw;
|
||||
max-width: none;
|
||||
margin-left: 50%;
|
||||
margin-right: 0;
|
||||
/* kill the UA figure margin, it overflowed the body */
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* A paragraph wrapping only a wide image must not add its line height. */
|
||||
p:has(> img.wide:only-child) {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
body.editing figure:has(.wide),
|
||||
body.editing img.wide {
|
||||
width: calc(100vw - var(--editor-w) - 1rem);
|
||||
}
|
||||
|
||||
/* Scroll reveal (pagerite.js adds .reveal/.in; JS off = fully visible) */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
translate: 0 14px;
|
||||
transition:
|
||||
opacity 0.6s ease,
|
||||
translate 0.6s ease;
|
||||
}
|
||||
|
||||
.reveal.in {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
.reveal {
|
||||
opacity: 1;
|
||||
translate: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-list-item {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* The footer element is kept (the editor host ends above it) but currently
|
||||
empty and zero-height. */
|
||||
footer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Rotating-cube page transition, adapted from termotohtori.fi.
|
||||
FRAGILE: do not tweak; the view-transition pseudo-tree is picky. */
|
||||
::view-transition {
|
||||
perspective: 1000px;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
::view-transition-group(root),
|
||||
::view-transition-image-pair(root) {
|
||||
transform-style: preserve-3d;
|
||||
isolation: auto;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
backface-visibility: hidden;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes group-rotate {
|
||||
to {
|
||||
transform: rotateY(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out-a-bit {
|
||||
to {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-a-bit {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(root) {
|
||||
transform-origin: 50% 50% -50vw;
|
||||
animation: 300ms ease-in-out forwards group-rotate;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 300ms ease-in-out forwards fade-out-a-bit;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
transform-origin: 0 0;
|
||||
transform: rotateY(90deg);
|
||||
inset: 0 auto 0 100%;
|
||||
animation: 300ms ease-in-out forwards fade-in-a-bit;
|
||||
}
|
||||
|
||||
/* Reverse direction for browser back navigation (same geometry, mirrored). */
|
||||
@keyframes group-rotate-back {
|
||||
to {
|
||||
transform: rotateY(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-group(root) {
|
||||
animation-name: group-rotate-back;
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-new(root) {
|
||||
transform-origin: 100% 0;
|
||||
transform: rotateY(-90deg);
|
||||
inset: 0 100% 0 auto;
|
||||
}
|
||||
|
||||
/* Same-section navigation: a plain crossfade instead of the cube. These
|
||||
rules only override animation/geometry, leaving the FRAGILE block's
|
||||
perspective and layering untouched. The old snapshot stays fully opaque
|
||||
underneath while the new one fades in on top — never a dip to black. */
|
||||
@keyframes nav-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-group(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-old(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-new(root) {
|
||||
transform: none;
|
||||
inset: 0;
|
||||
animation: 200ms ease-in-out nav-fade-in;
|
||||
}
|
||||
Reference in New Issue
Block a user