Serve built assets under /_/assets, admin at /_/admin
Reorganize the URL space so all machinery lives under /_/: the Vite build (now with assetsDir: '', two ES-module entries and hashed shared assets: style.css, pygments.css, banner.svg, fonts moved from pagerite/static to frontend/src/assets) is served at /_/assets via frontend.route(app, "/_/assets") with cached="/", and the admin shell moves to /_/admin, leaving only "_" as a reserved top-level slug. A /favicon.ico route serves the file Vite copies from frontend/public. In dev, Vite proxies content pages and /_/admin to the backend. Editor changes: drop the noisy status line for a save-error indicator, reconnect the WebSocket with exponential backoff, and re-establish the connection from send() when it has dropped. The socket connects when an editor is opened (they mount on pen click), not before.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 360">
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#0b0918"/>
|
||||
<stop offset="0.6" stop-color="#241842"/>
|
||||
<stop offset="1" stop-color="#3d2b6b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="aur1" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#00d4c8" stop-opacity="0"/>
|
||||
<stop offset="0.5" stop-color="#00d4c8" stop-opacity="0.7"/>
|
||||
<stop offset="1" stop-color="#7c5cff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="aur2" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#7c5cff" stop-opacity="0"/>
|
||||
<stop offset="0.5" stop-color="#ff5c8a" stop-opacity="0.55"/>
|
||||
<stop offset="1" stop-color="#ffd75c" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1600" height="360" fill="url(#sky)"/>
|
||||
<g fill="#ffffff">
|
||||
<circle cx="120" cy="60" r="1.6" opacity="0.9"/>
|
||||
<circle cx="300" cy="30" r="1.1" opacity="0.7"/>
|
||||
<circle cx="470" cy="90" r="1.4" opacity="0.8"/>
|
||||
<circle cx="640" cy="45" r="1" opacity="0.6"/>
|
||||
<circle cx="820" cy="70" r="1.5" opacity="0.85"/>
|
||||
<circle cx="990" cy="35" r="1.1" opacity="0.7"/>
|
||||
<circle cx="1150" cy="85" r="1.6" opacity="0.9"/>
|
||||
<circle cx="1320" cy="50" r="1" opacity="0.6"/>
|
||||
<circle cx="1480" cy="95" r="1.3" opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M0 190 Q 400 90 800 170 T 1600 150 V240 Q 1200 210 800 240 T 0 250 Z" fill="url(#aur1)"/>
|
||||
<path d="M0 230 Q 400 140 800 210 T 1600 190 V280 Q 1200 250 800 280 T 0 290 Z" fill="url(#aur2)"/>
|
||||
<path d="M0 300 Q 400 260 800 300 T 1600 290 V360 H0 Z" fill="#12101c"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
@font-face { font-family: 'Fraunces'; font-weight: 100 1000; font-style: normal; font-display: swap; src: url('fraunces.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Literata'; font-weight: 100 900; font-style: normal; font-display: swap; src: url('literata.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Fira Code'; font-weight: 300 700; font-style: normal; font-display: swap; src: url('firacode.woff2') format('woff2'); }
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,86 @@
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: #6e7681; background-color: #0d1117; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #6e7681; background-color: #0d1117; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #e6edf3; background-color: #6e7681; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #e6edf3; background-color: #6e7681; padding-left: 5px; padding-right: 5px; }
|
||||
pre code .hll { background-color: #6e7681 }
|
||||
pre code { color: #E6EDF3 }
|
||||
pre code .c { color: #8B949E; font-style: italic } /* Comment */
|
||||
pre code .err { color: #F85149 } /* Error */
|
||||
pre code .esc { color: #E6EDF3 } /* Escape */
|
||||
pre code .g { color: #E6EDF3 } /* Generic */
|
||||
pre code .k { color: #FF7B72 } /* Keyword */
|
||||
pre code .l { color: #A5D6FF } /* Literal */
|
||||
pre code .n { color: #E6EDF3 } /* Name */
|
||||
pre code .o { color: #FF7B72; font-weight: bold } /* Operator */
|
||||
pre code .x { color: #E6EDF3 } /* Other */
|
||||
pre code .p { color: #E6EDF3 } /* Punctuation */
|
||||
pre code .ch { color: #8B949E; font-style: italic } /* Comment.Hashbang */
|
||||
pre code .cm { color: #8B949E; font-style: italic } /* Comment.Multiline */
|
||||
pre code .cp { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Preproc */
|
||||
pre code .cpf { color: #8B949E; font-style: italic } /* Comment.PreprocFile */
|
||||
pre code .c1 { color: #8B949E; font-style: italic } /* Comment.Single */
|
||||
pre code .cs { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
pre code .gd { color: #FFA198; background-color: #490202 } /* Generic.Deleted */
|
||||
pre code .ge { color: #E6EDF3; font-style: italic } /* Generic.Emph */
|
||||
pre code .ges { color: #E6EDF3; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
pre code .gr { color: #FFA198 } /* Generic.Error */
|
||||
pre code .gh { color: #79C0FF; font-weight: bold } /* Generic.Heading */
|
||||
pre code .gi { color: #56D364; background-color: #0F5323 } /* Generic.Inserted */
|
||||
pre code .go { color: #8B949E } /* Generic.Output */
|
||||
pre code .gp { color: #8B949E } /* Generic.Prompt */
|
||||
pre code .gs { color: #E6EDF3; font-weight: bold } /* Generic.Strong */
|
||||
pre code .gu { color: #79C0FF } /* Generic.Subheading */
|
||||
pre code .gt { color: #FF7B72 } /* Generic.Traceback */
|
||||
pre code .g-Underline { color: #E6EDF3; text-decoration: underline } /* Generic.Underline */
|
||||
pre code .kc { color: #79C0FF } /* Keyword.Constant */
|
||||
pre code .kd { color: #FF7B72 } /* Keyword.Declaration */
|
||||
pre code .kn { color: #FF7B72 } /* Keyword.Namespace */
|
||||
pre code .kp { color: #79C0FF } /* Keyword.Pseudo */
|
||||
pre code .kr { color: #FF7B72 } /* Keyword.Reserved */
|
||||
pre code .kt { color: #FF7B72 } /* Keyword.Type */
|
||||
pre code .ld { color: #79C0FF } /* Literal.Date */
|
||||
pre code .m { color: #A5D6FF } /* Literal.Number */
|
||||
pre code .s { color: #A5D6FF } /* Literal.String */
|
||||
pre code .na { color: #E6EDF3 } /* Name.Attribute */
|
||||
pre code .nb { color: #E6EDF3 } /* Name.Builtin */
|
||||
pre code .nc { color: #F0883E; font-weight: bold } /* Name.Class */
|
||||
pre code .no { color: #79C0FF; font-weight: bold } /* Name.Constant */
|
||||
pre code .nd { color: #D2A8FF; font-weight: bold } /* Name.Decorator */
|
||||
pre code .ni { color: #FFA657 } /* Name.Entity */
|
||||
pre code .ne { color: #F0883E; font-weight: bold } /* Name.Exception */
|
||||
pre code .nf { color: #D2A8FF; font-weight: bold } /* Name.Function */
|
||||
pre code .nl { color: #79C0FF; font-weight: bold } /* Name.Label */
|
||||
pre code .nn { color: #FF7B72 } /* Name.Namespace */
|
||||
pre code .nx { color: #E6EDF3 } /* Name.Other */
|
||||
pre code .py { color: #79C0FF } /* Name.Property */
|
||||
pre code .nt { color: #7EE787 } /* Name.Tag */
|
||||
pre code .nv { color: #79C0FF } /* Name.Variable */
|
||||
pre code .ow { color: #FF7B72; font-weight: bold } /* Operator.Word */
|
||||
pre code .pm { color: #E6EDF3 } /* Punctuation.Marker */
|
||||
pre code .w { color: #6E7681 } /* Text.Whitespace */
|
||||
pre code .mb { color: #A5D6FF } /* Literal.Number.Bin */
|
||||
pre code .mf { color: #A5D6FF } /* Literal.Number.Float */
|
||||
pre code .mh { color: #A5D6FF } /* Literal.Number.Hex */
|
||||
pre code .mi { color: #A5D6FF } /* Literal.Number.Integer */
|
||||
pre code .mo { color: #A5D6FF } /* Literal.Number.Oct */
|
||||
pre code .sa { color: #79C0FF } /* Literal.String.Affix */
|
||||
pre code .sb { color: #A5D6FF } /* Literal.String.Backtick */
|
||||
pre code .sc { color: #A5D6FF } /* Literal.String.Char */
|
||||
pre code .dl { color: #79C0FF } /* Literal.String.Delimiter */
|
||||
pre code .sd { color: #A5D6FF } /* Literal.String.Doc */
|
||||
pre code .s2 { color: #A5D6FF } /* Literal.String.Double */
|
||||
pre code .se { color: #79C0FF } /* Literal.String.Escape */
|
||||
pre code .sh { color: #79C0FF } /* Literal.String.Heredoc */
|
||||
pre code .si { color: #A5D6FF } /* Literal.String.Interpol */
|
||||
pre code .sx { color: #A5D6FF } /* Literal.String.Other */
|
||||
pre code .sr { color: #79C0FF } /* Literal.String.Regex */
|
||||
pre code .s1 { color: #A5D6FF } /* Literal.String.Single */
|
||||
pre code .ss { color: #A5D6FF } /* Literal.String.Symbol */
|
||||
pre code .bp { color: #E6EDF3 } /* Name.Builtin.Pseudo */
|
||||
pre code .fm { color: #D2A8FF; font-weight: bold } /* Name.Function.Magic */
|
||||
pre code .vc { color: #79C0FF } /* Name.Variable.Class */
|
||||
pre code .vg { color: #79C0FF } /* Name.Variable.Global */
|
||||
pre code .vi { color: #79C0FF } /* Name.Variable.Instance */
|
||||
pre code .vm { color: #79C0FF } /* Name.Variable.Magic */
|
||||
pre code .il { color: #A5D6FF } /* Literal.Number.Integer.Long */
|
||||
@@ -0,0 +1,695 @@
|
||||
/* Shared styles for server-rendered pages and Vue components. */
|
||||
@import url("./fonts/fonts.css");
|
||||
@import url("./pygments.css");
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #12101c;
|
||||
--surface: #1b1830;
|
||||
--text: #e8e6f2;
|
||||
--muted: #9a94b8;
|
||||
--accent: #00d4c8;
|
||||
--accent2: #7c5cff;
|
||||
--line: #ffffff1a;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent2);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: none;
|
||||
filter: drop-shadow(0 0 0.1rem #000);
|
||||
}
|
||||
|
||||
/* 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%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Site structure tree: while dragging, empty child lists appear as drop
|
||||
zones so a page can be moved under a childless page. */
|
||||
body.tree-dragging .treelist:empty {
|
||||
min-height: 1.2rem;
|
||||
outline: 1px dashed var(--line);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
opacity: 0.55;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.banner-edit-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#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 render emoji checkmarks (see markdown.py), no diamond. */
|
||||
article .task-list-item::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
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;
|
||||
background: #ffffff09;
|
||||
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;
|
||||
background: #000;
|
||||
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