Files
bindery/bindery/static/style.css
T

905 lines
15 KiB
CSS

:root {
color-scheme: light dark;
--bg: #f4f5f7;
--surface: #ffffff;
--header-bg: #eaf0f8;
--card-bg: #fafafa;
--text: #1d2330;
--muted: #5f6878;
--border: #d9dde4;
--accent: #2f6fd6;
--accent-text: #ffffff;
--ok: #1f7a45;
--ok-bg: #e3f4ea;
--warn: #8a5a00;
--warn-bg: #fff2d6;
--err: #b3261e;
--err-bg: #fbe4e2;
--info: #22529e;
--info-bg: #e2ecfb;
--ins-bg: #dcf5e3;
--del-bg: #fbe0de;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
/* Text sizes. They use rem, so they do not shrink when one small element sits in another. */
--text-sm: 0.875rem;
--text-xs: 0.8rem;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14171c;
--surface: #1d2128;
--header-bg: #202b3d;
--card-bg: #262626;
--text: #e3e6eb;
--muted: #9aa3b2;
--border: #343a45;
--accent: #6b9cf0;
--accent-text: #0e1420;
--ok: #74d49a;
--ok-bg: #1d3527;
--warn: #f0c060;
--warn-bg: #3a2f14;
--err: #f28b82;
--err-bg: #3d1f1d;
--info: #9cbcf5;
--info-bg: #1f2c44;
--ins-bg: #1c3a26;
--del-bg: #43211f;
}
}
* {
box-sizing: border-box;
}
html {
font-size: 15px;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 1rem/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
a {
color: var(--accent);
}
a:focus-visible,
button:focus-visible,
summary:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
h1,
h2,
p {
margin: 0 0 8px;
}
h1,
h2 {
line-height: 1.3;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.1rem;
margin-bottom: 10px;
}
h1 + h2 {
margin-top: 16px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: 6px 8px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
th {
font-weight: 600;
color: var(--muted);
font-size: var(--text-sm);
white-space: nowrap;
}
tbody tr:last-child > td,
tbody tr:last-child > th {
border-bottom: 0;
}
td.actions {
white-space: nowrap;
}
td.actions button + button {
margin-left: 6px;
}
fieldset {
border: 0;
margin: 0 0 12px;
padding: 0;
}
legend {
font-weight: 600;
padding: 0;
margin-bottom: 4px;
}
input[type='checkbox'] {
width: 1rem;
height: 1rem;
margin: 0;
vertical-align: -2px;
accent-color: var(--accent);
}
progress {
width: 100%;
margin-top: 6px;
}
/* Layout */
header,
main {
width: min(1100px, calc(100% - 32px));
margin: 0px auto;
}
header,
main:not(.login) {
background: var(--surface);
border: 1px solid var(--border);
box-shadow: 0 4px 16px rgb(0 0 0 / 5%);
}
main:not(.login) {
border-top: 0px;
}
header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px 24px;
padding: 20px 24px 0;
background: var(--header-bg);
border-top-left-radius: 12px;
border-top-right-radius: 12px;
margin-top: 8px;
}
/* min-width: 0 lets the nav get narrower than its tabs, so on a phone the story tab shrinks
to an ellipsis instead of pushing the nav past the edge of the header. */
header nav {
display: flex;
flex-basis: 100%;
min-width: 0;
gap: 4px;
margin-bottom: -1px;
}
header nav a {
padding: 10px 18px;
border: 1px solid var(--border);
border-radius: 8px 8px 0 0;
background: var(--bg);
color: var(--muted);
font-weight: 600;
text-decoration: none;
}
header nav a:hover {
background: var(--surface);
color: var(--text);
}
header nav a[aria-current='page'] {
background: var(--surface);
border-bottom-color: var(--surface);
color: var(--text);
}
/* A long story title ends in an ellipsis. The title attribute shows all of it. */
header nav a.story-tab {
max-width: 24ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand {
display: flex;
align-items: center;
gap: 8px;
font-size: 1.1rem;
font-weight: 700;
color: var(--text);
text-decoration: none;
}
header .brand {
font-size: 1.65rem;
line-height: 1.2;
letter-spacing: -0.025em;
}
header .brand img {
width: 32px;
height: 32px;
}
main {
padding: 20px;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
margin-bottom: 8px;
}
main > :last-child {
margin-bottom: 0;
}
main.login {
max-width: 400px;
margin-top: 64px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 16px;
}
@media (max-width: 600px) {
header,
main {
width: calc(100% - 24px);
padding: 12px;
}
header {
padding-bottom: 0;
}
.card {
padding: 14px;
}
}
.card-danger {
border-color: var(--err);
}
details > summary {
cursor: pointer;
}
/* Adjoining margins collapse to the largest one, and the fieldset above ends in a p.hint with
an 8px bottom margin. These both go to 0, so that 8px is the whole gap: the smallest this
space can be without changing the hint's own margin. */
fieldset:has(+ .force-fields) {
margin-bottom: 0;
}
.force-fields {
margin: 0 0 12px;
}
/* A card that folds shut, such as "Import chapters from an epub". Scoped to .card so it does not
change the small "Site value differs" fold of a metadata field. */
details.card > summary {
list-style: none;
display: flex;
align-items: center;
gap: 8px;
}
details.card > summary::-webkit-details-marker {
display: none;
}
details.card > summary::before {
content: '▸';
flex: none;
color: var(--muted);
transition: transform 0.15s ease;
}
details.card[open] > summary::before {
transform: rotate(90deg);
}
details.card > summary h2 {
flex: 1;
min-width: 0;
/* Without this, the h2's own bottom margin throws off the vertical centering of the
arrow next to it, and leaves extra space under the heading. */
margin: 0;
}
details[open] > summary {
margin-bottom: 10px;
}
/* The Download card offers this button even while closed. Once open, the controls below repeat it. */
details.card[open] > summary .summary-action {
display: none;
}
.controls,
.section-heading,
.account,
.job-heading,
.rule-actions,
.field-overrides,
.library-search,
.library-results > li {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
}
.controls,
.field-overrides,
.library-search,
.library-results > li {
margin-top: 8px;
}
.section-heading {
justify-content: space-between;
}
/* Stays visible while the chapter list scrolls, so the buttons need no scrolling back up to
reach, but only once a chapter is selected. Otherwise it would cover the top of the list for
no reason. It stops following once the Chapters card itself scrolls out of view. */
.card:has(.chapters-table input:checked) .chapter-actions {
position: sticky;
top: 0;
z-index: 1;
background: var(--card-bg);
padding: 8px 0;
border-bottom: 1px solid var(--border);
/* Without its own compositing layer, some phone browsers repaint a stuck element with
several stacked children wrong while scrolling, showing only the last child. */
transform: translateZ(0);
}
.login form,
.login label:has(> input:not([type='checkbox'])) {
display: flex;
flex-direction: column;
gap: 8px;
}
.account {
margin-left: auto;
color: var(--muted);
font-size: var(--text-sm);
}
.library-search > input {
flex-grow: 1;
}
label:has(> input[type='radio']) {
display: block;
}
.library-results {
list-style: none;
margin: 8px 0 0;
padding: 0;
}
/* Text */
.hint {
max-width: 80ch;
color: var(--muted);
font-size: var(--text-sm);
margin: 4px 0 8px;
}
.controls .hint,
.controls .error {
margin: 0;
}
.notice {
margin: 12px 0;
padding: 10px 14px;
border-left: 3px solid var(--warn);
border-radius: 6px;
background: var(--warn-bg);
}
.notice p {
margin: 0;
}
.notice ul {
margin: 4px 0 0;
padding-left: 20px;
}
.byline,
.last-check,
.story-site,
.field-default,
small,
.date,
.job time {
color: var(--muted);
}
small,
.date,
.job time,
.breadcrumbs {
font-size: var(--text-sm);
}
.breadcrumbs {
margin-bottom: 8px;
}
.ini,
.story-urls,
.rule input:not(.rule-note) {
font-family: var(--mono);
}
/* A monospace font looks larger than the text font at the same size. The line height keeps these fields as tall as
the note field in the same row. */
.rule input:not(.rule-note) {
font-size: var(--text-sm);
line-height: 1.5rem;
}
.date {
white-space: nowrap;
}
.error {
color: var(--err);
}
.error:empty {
display: none;
}
.empty {
color: var(--muted);
font-style: italic;
}
.chapter-excluded {
opacity: 0.6;
}
/* Controls */
button,
a[download] {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
font: inherit;
cursor: pointer;
text-decoration: none;
}
button:hover:not(:disabled),
a[download]:hover {
border-color: var(--accent);
}
button.primary:hover:not(:disabled) {
filter: brightness(1.08);
}
button.danger:hover:not(:disabled) {
border-color: var(--err);
background: var(--err-bg);
}
button:disabled {
opacity: 0.55;
cursor: default;
}
button.primary {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-text);
}
button.danger {
border-color: var(--err);
color: var(--err);
}
:is(.account, .actions, .rule-actions, .job, .library-results) button {
padding: 2px 10px;
font-size: var(--text-sm);
}
.field-site .controls button {
padding: 0;
border: 0;
background: none;
color: var(--accent);
text-decoration: underline;
}
/* One metadata field: a stacked card, so it stays readable on a narrow phone screen. */
.field-grid {
display: flex;
flex-direction: column;
gap: 14px;
}
.field-grid > .field {
padding-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.field-grid > .field:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.field-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 4px;
}
/* On a wide screen, the label sits beside the field instead of above it, so a short value such
as a title does not stretch across the whole card. */
@media (min-width: 700px) {
.field {
display: grid;
grid-template-columns: 11em minmax(0, 1fr);
column-gap: 16px;
}
.field-head {
flex-direction: column;
align-items: flex-start;
gap: 6px;
margin-bottom: 0;
}
.field > :not(.field-head) {
grid-column: 2;
}
}
.field-label {
font-weight: 600;
}
.lock-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 3px 10px;
border: 1px solid var(--border);
border-radius: 999px;
font-size: var(--text-sm);
color: var(--muted);
cursor: pointer;
}
.lock-toggle:has(input:checked) {
border-color: var(--info);
background: var(--info-bg);
color: var(--info);
}
/* The site's current value, folded away so a long description does not push other fields down. */
.field-site {
margin-top: 6px;
font-size: var(--text-sm);
}
.field-site summary {
cursor: pointer;
color: var(--muted);
}
.field-site p {
margin: 6px 0 0;
color: var(--text);
}
input:not([type='checkbox'], [type='radio']),
select,
textarea {
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
font: inherit;
min-width: 0;
max-width: 100%;
}
input:not([type='checkbox'], [type='radio']):focus,
select:focus,
textarea:focus {
outline: 2px solid var(--accent);
outline-offset: -1px;
}
input[type='number'] {
width: 6em;
}
textarea {
width: 100%;
resize: vertical;
line-height: 1.45;
}
.metadata input:not([type='checkbox']) {
width: 100%;
max-width: 32em;
}
.ini,
.story-urls {
font-size: var(--text-sm);
}
.badge {
display: inline-block;
padding: 0 7px;
border-radius: 10px;
font-size: var(--text-xs);
line-height: 1.6;
font-weight: 600;
background: var(--info-bg);
color: var(--info);
white-space: nowrap;
}
.badge-ok {
background: var(--ok-bg);
color: var(--ok);
}
.badge-err {
background: var(--err-bg);
color: var(--err);
}
.badge-warn {
background: var(--warn-bg);
color: var(--warn);
}
.badge-muted {
background: var(--bg);
color: var(--muted);
}
.badges .badge {
margin-right: 4px;
}
.table-wrap {
overflow-x: auto;
}
/* Jobs */
.jobs {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 420px;
overflow-y: auto;
}
.job {
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 10px;
}
.job-heading > time {
margin-left: auto;
}
.job > p {
margin: 0;
font-size: var(--text-sm);
overflow-wrap: anywhere;
}
/* Rules */
.rules {
display: flex;
flex-direction: column;
gap: 6px;
}
/* A rule is a card, like a row of the chapter list, the same at every width. The pattern and the
replacement each need the room of their own line, so nothing here shares a line with them. */
.rule {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
}
.rule-top,
.rule-bottom {
display: flex;
align-items: center;
gap: 6px;
}
/* Fits its 4-character maximum, and no more. */
.rule-flags {
width: 4.5em;
flex: none;
}
.rule-top .rule-note {
flex: 1;
}
.rule-actions {
display: flex;
flex: 1;
gap: 4px;
justify-content: flex-end;
}
/* The site cover sits at the top left of the story page, beside the title. */
.story-summary {
display: flex;
align-items: flex-start;
gap: 20px;
}
.story-heading {
flex: 1;
min-width: 0;
}
.story-summary .cover {
flex: none;
width: 140px;
height: auto;
border-radius: 6px;
box-shadow: 0 2px 8px rgb(0 0 0 / 20%);
}
@media (max-width: 600px) {
.story-summary {
gap: 12px;
}
.story-summary .cover {
width: 88px;
}
}
.rule-invalid {
outline: 2px solid var(--err);
}
.preview {
margin-top: 12px;
}
/* Toasts */
#toasts {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 8px;
max-width: min(480px, calc(100vw - 32px));
}
.toast {
padding: 10px 16px;
border: 1px solid var(--accent);
border-radius: 8px;
background: var(--surface);
white-space: pre-line;
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
}
.toast-error {
border-color: var(--err);
color: var(--err);
}
/* The server builds diff HTML with these classes, and escapes all text in it. */
.diff {
max-height: 70vh;
overflow-y: auto;
margin-top: 10px;
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 12px;
}
.diff p {
margin: 6px 0;
padding: 2px 6px;
border-radius: 4px;
overflow-wrap: anywhere;
}
.diff .diff-eq {
color: var(--muted);
}
.diff .diff-ins,
.diff ins {
background: var(--ins-bg);
text-decoration: none;
}
.diff .diff-del,
.diff del {
background: var(--del-bg);
}
.diff .diff-del {
text-decoration: line-through;
}
.diff .diff-skip {
text-align: center;
color: var(--muted);
font-style: italic;
font-size: var(--text-sm);
}