Files
mangaBookmark/backend/static/style.css
T
sulthan ce7e93df53 Self-host the Cinder webfonts
The design depends on three specific families, and loading them from
fonts.googleapis.com means the UI silently loses its character exactly
where it is used most: Bromite is a de-googled browser whose users
routinely block Google's font domains, and the backend is reachable over a
LAN with no internet route. In both cases the page fell back to Georgia
and a system sans.

Five latin-subset woff2 files, ~120 KB total, in a binary already 27 MB —
the cost is noise, and //go:embed static picks them up with no build
change (backend/Dockerfile already copies static/). Latin only because the
UI is English; woff2 only because every browser that can run this app
supports it, so there is no second format to carry. DM Sans ships as one
variable file, which covers the whole 400-700 range the design uses for
the price of a single request.

staticHandler registers the .woff2 MIME type: Go's built-in table has no
entry for it and the scratch image has no /etc/mime.types, so the fonts
would otherwise be served as application/octet-stream. The two faces
above the fold are preloaded, since they are now discovered a stylesheet
late rather than from a preconnect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:34:26 +07:00

658 lines
17 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Fonts are served from this binary, not from Google: this UI is read in
Bromite, where fonts.googleapis.com is routinely blocked, and over a LAN
with no internet route. Latin subsets only, woff2 only — every browser that
runs the app supports it, so there is no second format to ship. Roughly
120 KB for all five faces, embedded via //go:embed static. */
@font-face {
font-family: "Instrument Serif";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/instrument-serif-400-latin.woff2) format("woff2");
}
@font-face {
font-family: "Instrument Serif";
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(/static/fonts/instrument-serif-400-italic-latin.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/fonts/ibm-plex-mono-500-latin.woff2) format("woff2");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/static/fonts/ibm-plex-mono-600-latin.woff2) format("woff2");
}
/* One variable file covers the 400–700 range the design uses. */
@font-face {
font-family: "DM Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(/static/fonts/dm-sans-var-latin.woff2) format("woff2");
}
/* Cinder — editorial serif, containerless sheets, ash hairlines.
Heat is purely typographic: a series with an unread chapter turns its title
crimson and gains an ember underline. Everything else stays cool. Dark is
the default because this is read at night on a phone; the light branch
re-tunes the same tokens onto paper. */
:root {
color-scheme: dark light;
--font-display: "Instrument Serif", Georgia, "Times New Roman", serif;
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--font-body: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
--ink: #100f0e; /* page */
--ash: #161413; /* recessed panel (chapter form) */
--dim: #0d0c0b; /* archived / finished rows sink */
--rule: #221f1d; /* hairline between sheets */
--rule-soft: #1a1817; /* measure edges */
--field-line: #2c2926;
--hover: #1a1816;
--paper: #f2ece5; /* highest-contrast text, primary button */
--paper-hot: #f0d3cb; /* title of a series with a new chapter */
--paper-dim: #ddd5cb; /* resting title */
--mute: #8d857c; /* secondary text, idle icons */
--mute-2: #5a5450; /* labels, hints */
--faint: #3a3733; /* meta separators */
--faint-2: #57504b; /* cover monogram */
--ember: #e0452c; /* the only heat */
--ember-wash: #1a1211; /* ember-tinted surface */
--ember-ink: #150907; /* text on solid ember */
--ember-soft: #eda798; /* text on ember wash */
--brass: #b8912f; /* favourite — a second, cooler metal */
--trash: #6b5450; /* remove, resting */
--asura: #7d93a5;
--demonic: #a98a78;
/* Covers are often missing; the hatch keeps the slot honest instead of
faking artwork. */
--hatch: repeating-linear-gradient(135deg, #211d1b 0 5px, #191614 5px 10px);
--hatch-dim: repeating-linear-gradient(135deg, #1b1918 0 5px, #151313 5px 10px);
--measure: 760px;
}
/* Light mode: same rules, cooler paper. Hues are re-tuned, not reused — the
dark ember is too bright to read as text on off-white. */
@media (prefers-color-scheme: light) {
:root {
--ink: #f7f4ef;
--ash: #efeae3;
--dim: #f1ede7;
--rule: #e0dad2;
--rule-soft: #e8e3dc;
--field-line: #d4cdc4;
--hover: #efeae3;
--paper: #191715;
--paper-hot: #a33018;
--paper-dim: #191715;
--mute: #6b645d;
--mute-2: #857d75;
--faint: #c9c2ba;
--faint-2: #a8a098;
--ember: #c23a22;
--ember-wash: #fbeee9;
--ember-ink: #fff;
--ember-soft: #8d2c17;
--brass: #8a681c;
--trash: #a98276;
--asura: #4f6b80;
--demonic: #8a6a55;
--hatch: repeating-linear-gradient(135deg, #e6e0d8 0 5px, #efeae3 5px 10px);
--hatch-dim: repeating-linear-gradient(135deg, #ebe6de 0 5px, #f2eee8 5px 10px);
}
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ink);
color: var(--paper-dim);
font: 400 15px/1.5 var(--font-body);
display: flex;
justify-content: center;
-webkit-text-size-adjust: 100%;
}
a { color: inherit; text-decoration: none; }
button, input { font: inherit; }
button { cursor: pointer; }
/* Every hideable thing here is a flex container, and display beats hidden. */
[hidden] { display: none !important; }
@keyframes barSlide { from { transform: translateX(-100%) } to { transform: translateX(320%) } }
@keyframes sheetIn { from { opacity: 0; transform: translateY(-4px) } to { opacity: 1; transform: none } }
@keyframes emberPulse { 0%, 100% { opacity: .5 } 50% { opacity: 1 } }
/* One measured column, edges drawn rather than boxed. */
.sheet {
width: 100%;
max-width: var(--measure);
min-height: 100vh;
display: flex;
flex-direction: column;
border-left: 1px solid var(--rule-soft);
border-right: 1px solid var(--rule-soft);
padding-bottom: env(safe-area-inset-bottom);
}
/* ---- brand + chrome ---- */
.brand {
margin: 0;
font: 400 26px/1 var(--font-display);
color: var(--paper);
}
.brand em { color: var(--ember); font-style: italic; }
.topbar {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
padding: 22px 20px 14px;
}
.topbar form { margin: 0; }
.ghost {
padding: 0;
border: none;
border-bottom: 1px solid var(--field-line);
background: transparent;
color: var(--mute);
font: 500 11px var(--font-mono);
letter-spacing: .1em;
text-transform: uppercase;
}
.ghost:hover { color: var(--paper); border-bottom-color: var(--paper); }
.chrome { display: flex; flex-direction: column; }
.searchbar {
display: flex;
align-items: center;
gap: 10px;
height: 44px;
margin: 0 20px 14px;
border-bottom: 1px solid var(--rule);
color: var(--mute-2);
}
.searchbar svg { width: 15px; height: 15px; flex: none; }
.search {
flex: 1;
min-width: 0;
height: 44px;
border: none;
background: transparent;
color: var(--paper);
font: 400 15px var(--font-body);
outline: none;
}
.search::placeholder { color: var(--mute-2); }
/* Tabs are set in the display serif and underlined, not chipped. */
.tabs {
display: flex;
gap: 16px;
padding: 2px 20px 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
border-bottom: 1px solid var(--rule);
}
.tabs::-webkit-scrollbar { display: none; }
.tabs [role="tab"] {
flex: none;
display: flex;
align-items: center;
gap: 7px;
padding: 8px 0 12px;
color: var(--mute);
font: 400 17px var(--font-display);
white-space: nowrap;
}
.tabs [role="tab"]:hover { color: var(--paper-dim); }
.tabs [role="tab"].active {
color: var(--paper);
border-bottom: 2px solid var(--paper);
margin-bottom: -1px;
}
/* Updated is the one tab that carries heat. */
.tabs .tab-new { color: var(--ember); }
.tabs .tab-new.active { border-bottom-color: var(--ember); }
.count {
font: 600 10px var(--font-mono);
letter-spacing: 0;
padding: 2px 5px;
border: 1px solid currentColor;
}
/* ---- continue reading ---- */
.recent {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px 0 18px;
border-bottom: 1px solid var(--rule);
}
.recent h2 {
margin: 0;
padding: 0 20px;
font: 500 10px/1 var(--font-mono);
letter-spacing: .2em;
text-transform: uppercase;
color: var(--mute-2);
}
.recent-strip {
display: flex;
gap: 14px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
padding: 0 20px 4px;
}
.recent-strip::-webkit-scrollbar { display: none; }
.recent-card {
width: 92px;
flex: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.recent-cover {
position: relative;
width: 92px;
height: 123px;
background: var(--hatch);
display: grid;
place-items: center;
overflow: hidden;
}
.recent-cover img { width: 100%; height: 100%; object-fit: cover; }
.recent-title {
font: 400 15px/1.2 var(--font-display);
color: var(--paper-dim);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.recent-chapter {
font: 500 10px/1 var(--font-mono);
letter-spacing: .1em;
color: var(--mute-2);
}
.recent-card.is-new .recent-title { color: var(--paper-hot); }
.recent-card.is-new .recent-chapter { color: var(--ember); }
/* The rule at the cover foot is the only cover ornament: ember for a new
chapter, brass for a favourite. */
.foot-rule {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
background: var(--ember);
}
.foot-rule.brass { background: var(--brass); }
.monogram {
font: 400 32px var(--font-display);
color: var(--faint-2);
}
/* ---- rows ---- */
.list { display: flex; flex-direction: column; }
.card {
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px 20px;
border-bottom: 1px solid var(--rule);
animation: sheetIn .18s ease-out;
}
.card.is-dim { background: var(--dim); }
.row { display: flex; flex-wrap: wrap; gap: 14px; }
.cover {
position: relative;
width: 66px;
height: 88px;
flex: none;
background: var(--hatch);
display: grid;
place-items: center;
overflow: hidden;
}
.cover img { width: 100%; height: 100%; object-fit: cover; }
.cover .monogram { font-size: 24px; }
.is-dim .cover { background: var(--hatch-dim); filter: grayscale(1); opacity: .85; }
.body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.title-line { display: flex; gap: 8px; align-items: flex-start; }
.title {
margin: 0;
flex: 1;
min-width: 0;
font: 400 19px/1.2 var(--font-display);
color: var(--paper-dim);
}
/* Heat: crimson title over an ember hairline sized to the text, not the row. */
.is-new .title {
flex: 0 1 auto;
align-self: flex-start;
color: var(--paper-hot);
border-bottom: 1px solid var(--ember);
padding-bottom: 3px;
}
.is-dim .title { font-style: italic; color: var(--mute); }
.is-dim .fav-mark { color: var(--mute-2); }
.fav-mark { flex: none; width: 13px; height: 13px; margin-top: 5px; color: var(--brass); }
.meta {
margin: 0;
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
font: 500 10px/1 var(--font-mono);
letter-spacing: .12em;
text-transform: uppercase;
color: var(--mute);
}
.meta .sep { color: var(--faint); }
.site-asura { color: var(--asura); }
.site-demonic { color: var(--demonic); }
.new-chapter { color: var(--ember); }
.state { display: flex; align-items: center; gap: 4px; color: var(--mute); }
.state svg { width: 9px; height: 9px; }
.is-dim .meta { color: var(--mute-2); }
.is-dim .site-asura, .is-dim .site-demonic { color: var(--mute); filter: grayscale(.6); }
/* ---- action strip: full-width on a phone, hairline-divided cells ---- */
.actions {
flex: 1 0 100%;
display: flex;
border-top: 1px solid var(--rule);
}
.actions > * {
flex: 1 1 0;
min-width: 0;
height: 46px;
display: grid;
place-items: center;
padding: 0;
border: none;
border-right: 1px solid var(--rule);
background: transparent;
color: var(--mute);
}
.actions > *:last-child { border-right: none; }
.actions svg { width: 17px; height: 17px; }
.actions > *:hover { color: var(--paper); }
.actions .play { color: var(--paper); }
.is-new .actions .play { color: var(--ember); }
.actions .play:hover { background: var(--hover); }
.actions .on { color: var(--brass); }
.actions .restore { color: var(--paper); }
.actions .remove { color: var(--trash); }
.actions .remove:hover { color: var(--ember); }
.actions .open { background: var(--hover); color: var(--paper); }
.actions .remove.open { background: var(--ember-wash); color: var(--ember); }
.is-dim .actions > * { color: var(--mute-2); }
/* ---- disclosure panels ---- */
.chapter-form, .confirm-row, .error-inline { animation: sheetIn .18s ease-out; }
.chapter-form {
display: flex;
flex-direction: column;
gap: 10px;
margin: 0;
padding: 14px;
background: var(--ash);
}
.chapter-form .hint {
margin: 0;
font: 500 10px/1 var(--font-mono);
letter-spacing: .14em;
text-transform: uppercase;
color: var(--mute-2);
}
.chapter-form .field { display: flex; gap: 10px; }
.chapter-form input {
flex: 1;
min-width: 0;
height: 46px;
padding: 0 12px;
border: 1px solid var(--field-line);
background: var(--ink);
color: var(--paper);
font: 500 17px var(--font-mono);
outline: none;
}
.chapter-form input:focus { border-color: var(--ember); }
.chapter-form input::-webkit-outer-spin-button,
.chapter-form input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.chapter-form button {
height: 46px;
padding: 0 20px;
border: none;
background: var(--paper);
color: var(--ink);
font: 400 17px var(--font-display);
}
.confirm-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px;
background: var(--ember-wash);
}
.confirm-row span { font: 400 17px var(--font-display); color: var(--ember-soft); }
.confirm-row div { display: flex; gap: 8px; }
.confirm-row button {
height: 40px;
padding: 0 14px;
border: 1px solid var(--field-line);
background: transparent;
color: var(--mute);
font: 500 12px var(--font-body);
}
.confirm-row button:hover { color: var(--paper); }
.confirm-row .danger-solid {
border: none;
background: var(--ember);
color: var(--ember-ink);
font-weight: 600;
}
.error-inline {
display: flex;
align-items: center;
gap: 9px;
margin: 0;
padding: 11px 13px;
background: var(--ember-wash);
border-left: 2px solid var(--ember);
font: 400 16px var(--font-display);
color: var(--ember-soft);
}
.error-inline::before {
content: "";
flex: none;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--ember);
animation: emberPulse 1.4s ease-in-out infinite;
}
/* Busy: the hairline at the top of the sheet burns across it. */
.card.htmx-request { overflow: hidden; }
.card.htmx-request .actions { pointer-events: none; opacity: .5; }
.card.htmx-request::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 30%;
height: 1px;
background: var(--ember);
animation: barSlide 1.15s linear infinite;
}
/* ---- empty ---- */
.empty {
display: flex;
flex-direction: column;
gap: 7px;
padding: 40px 20px 48px;
}
.empty strong { font: 400 20px var(--font-display); color: var(--paper); }
.empty.hot strong { color: var(--ember); }
.empty p {
margin: 0;
max-width: 44ch;
font: 400 14px/1.6 var(--font-body);
color: var(--mute);
text-wrap: pretty;
}
/* ---- login ---- */
.login-card {
width: 100%;
max-width: 420px;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 52px 28px 40px;
}
.login-card .eyebrow {
font: 500 10px/1 var(--font-mono);
letter-spacing: .2em;
text-transform: uppercase;
color: var(--mute-2);
}
.login-card h1 {
margin: 14px 0 0;
font: 400 44px/1 var(--font-display);
letter-spacing: -.01em;
color: var(--paper);
}
.login-card h1 em { color: var(--ember); font-style: italic; }
.login-card form { display: flex; flex-direction: column; gap: 18px; }
.login-card label {
font: 500 10px/1 var(--font-mono);
letter-spacing: .16em;
text-transform: uppercase;
color: var(--mute);
}
.login-card input {
width: 100%;
height: 54px;
margin-top: 9px;
padding: 0 2px;
border: none;
border-bottom: 1px solid var(--field-line);
background: transparent;
color: var(--paper);
font: 500 20px var(--font-mono);
letter-spacing: .16em;
outline: none;
}
.login-card input:focus { border-bottom-color: var(--ember); }
.login-card .error {
margin: 0;
min-height: 20px;
font: 400 13px/1.4 var(--font-body);
color: var(--ember);
}
.login-card button {
height: 54px;
border: 1px solid var(--paper);
background: var(--paper);
color: var(--ink);
font: 400 19px var(--font-display);
}
.login-card button:hover {
background: var(--ember);
border-color: var(--ember);
color: #fff;
}
/* ---- desktop: same measure, actions fold up beside the row ---- */
@media (min-width: 720px) {
.topbar { padding: 26px 32px 18px; }
.brand { font-size: 30px; }
.chrome {
flex-direction: row;
align-items: center;
gap: 26px;
padding: 0 32px;
border-bottom: 1px solid var(--rule);
}
.tabs { order: 1; flex: none; gap: 20px; padding: 0; border-bottom: none; }
.tabs [role="tab"] { padding: 10px 0 14px; font-size: 18px; }
.searchbar { order: 2; flex: 1; margin: 0; border-bottom: none; }
.recent h2, .recent-strip { padding-left: 32px; padding-right: 32px; }
.recent-card, .recent-cover { width: 100px; }
.recent-cover { height: 133px; }
.card { padding: 18px 32px; }
.row { flex-wrap: nowrap; align-items: center; gap: 20px; }
.cover { width: 74px; height: 100px; }
.cover .monogram { font-size: 26px; }
.title { font-size: 21px; }
.actions { flex: none; gap: 4px; border-top: none; }
.actions > * {
flex: none;
width: 40px;
height: 40px;
border: 1px solid var(--rule);
}
.is-new .actions .play { border-color: #3a1d18; }
.actions .on { border-color: #332b14; }
.chapter-form, .confirm-row, .error-inline { margin-left: 94px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}