Files
mangaBookmark/backend/internal/web/static/admin.css
T
sulthan bc64a1d894 backend: remove orphan series from the admin (ticket #155)
(*Store).RemoveSeries deletes one series row by (site, series_id) via a
plain parameterized DELETE; a bookmarks_series_fk violation outside
23503 is translated into the ErrSeriesHasBookmarks sentinel so no
driver type escapes the store. The caller reads the row's cover
address before the delete and reclaims it after: ReclaimCover's guard
cannot pass while a series row still points at the address.

POST /admin/series/{key}/remove answers the list row with the removed
row's fragment plus the heading re-rendered out of band at the fresh
count (HX-Reswap: delete removes the row through the same button that
swaps the refusal back in), and navigates from the detail page to the
No-Readers list (HX-Redirect for htmx, a 303 for plain clients). A
removal that races a fresh Bookmark is a refusal, not a 500: the row
re-renders at its new count with the fact spelled out. The control
renders only at zero Reader count on both surfaces, gated by hx-confirm
with the brief's copy.
2026-08-22 09:55:01 +07:00

859 lines
16 KiB
CSS

:root {
--measure-wide: 1080px;
}
@media (prefers-color-scheme: light) {
:root {
--measure-wide: 1080px;
}
}
.admin-sheet {
max-width: var(--measure-wide);
}
.admin-sheet .brand em {
color: var(--patina);
}
.admin-sheet .brand .mark > g > g:last-child {
stroke: var(--patina);
}
.topbar-actions {
display: flex;
align-items: center;
gap: 18px;
margin-left: auto;
}
.navrow {
display: flex;
gap: 18px;
padding: 2px 20px 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
border-bottom: 1px solid var(--rule);
}
.navrow::-webkit-scrollbar {
display: none;
}
.navrow a {
flex: none;
display: flex;
align-items: center;
min-width: 44px;
padding: 8px 0 12px;
color: var(--mute);
font: 400 17px var(--font-display);
white-space: nowrap;
}
.navrow a:hover {
color: var(--paper-dim);
}
.navrow a.active {
color: var(--paper);
border-bottom: 2px solid var(--paper);
margin-bottom: -1px;
}
.admin-page {
padding: 0 20px 40px;
}
.admin-page > .sec,
.admin-sheet .readers h2,
.admin-sheet .lanes h2 {
position: relative;
margin: 0;
padding: 26px 0 8px;
font: 500 11px/1 var(--font-mono);
letter-spacing: .2em;
text-transform: uppercase;
color: var(--mute-2);
}
.admin-page > .sec::before,
.admin-sheet .readers h2::before,
.admin-sheet .lanes h2::before {
content: "";
position: absolute;
top: 14px;
left: 0;
width: 34px;
height: 2px;
background: var(--patina);
}
.admin-sheet .readers,
.admin-sheet .lanes {
margin: 0;
padding: 0 0 16px;
border-bottom: none;
}
.admin-sheet .readerlist,
.admin-sheet .lanelist {
margin: 0;
padding: 0;
list-style: none;
}
.admin-sheet .readerlist li,
.admin-sheet .lanelist li {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 16px;
min-height: 48px;
padding: 10px 0;
border-top: 1px solid var(--rule);
}
.admin-sheet .reader-actions {
display: flex;
flex: 0 0 auto;
gap: 18px;
margin-left: auto;
white-space: nowrap;
}
.admin-sheet .reader-actions .ghost,
.admin-sheet .c-act .ghost {
font-size: 12px;
color: var(--patina);
}
.admin-sheet .reader-actions .ghost.danger,
.admin-sheet .c-act .ghost.danger {
color: var(--danger);
}
.admin-sheet .readerlist form {
margin: 0;
}
.admin-sheet .lane-browser {
padding: 12px 0 0;
}
.admin-sheet .ghost.danger {
color: var(--danger);
}
.admin-sheet .ghost.danger:hover {
color: var(--danger);
border-bottom-color: var(--danger);
}
.admin-sheet .reader-id {
font: 500 15px/1.5 var(--font-mono);
letter-spacing: .01em;
color: var(--paper);
}
.admin-sheet .reader-sessions,
.admin-sheet .reader-sightings,
.admin-sheet .reader-blocked,
.admin-sheet .lane-fact,
.admin-sheet .lane-mark {
font: 500 13px/1.4 var(--font-mono);
letter-spacing: .06em;
text-transform: uppercase;
}
.admin-sheet .reader-sessions {
color: var(--paper-dim);
}
.admin-sheet .reader-sightings,
.admin-sheet .lane-fact {
color: var(--mute);
}
.admin-sheet .reader-blocked,
.admin-sheet .lane-mark {
color: var(--patina);
}
.admin-sheet .lane-site {
font: 400 19px/1.2 var(--font-display);
color: var(--paper-dim);
}
.admin-sheet .lanelist li.attention .lane-site {
color: var(--danger);
}
/* A single grid keeps row rules continuous; cell padding supplies gutters. */
.admin-sheet .sechead {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
}
.admin-sheet .sechead .statusline {
padding: 0 0 8px;
font-size: 11px;
letter-spacing: .14em;
text-transform: uppercase;
}
.admin-sheet .statusline {
margin: 0;
padding: 0 0 10px;
font: 500 12px/1 var(--font-mono);
letter-spacing: .04em;
color: var(--mute-2);
}
/* The verdict line is set in the data face, not the display face: it is
three counts, not a page title. The judgement is the only bright thing. */
.admin-sheet .verdict {
padding: 16px 0 12px;
border-bottom: 1px solid var(--rule);
font: 500 15px/1.6 var(--font-mono);
letter-spacing: .04em;
color: var(--mute);
}
.admin-sheet .verdict .attn {
color: var(--patina);
}
.admin-sheet .verdict .counts b {
color: var(--paper);
font-weight: 500;
}
.admin-sheet .tbl {
display: grid;
grid-template-columns: minmax(240px, 1fr) 156px 92px 110px 76px minmax(150px, 220px) 140px;
column-gap: 0;
font-variant-numeric: tabular-nums;
}
.admin-sheet .tbl .thead {
display: contents;
}
.admin-sheet .tbl .thead > * {
padding: 10px 14px 8px 0;
border-bottom: 1px solid var(--rule);
font: 500 12px/1 var(--font-mono);
letter-spacing: .12em;
text-transform: uppercase;
color: var(--mute-2);
}
.admin-sheet .tbl .trow {
display: contents;
}
.admin-sheet .tbl .trow > * {
padding: 11px 14px 11px 0;
border-bottom: 1px solid var(--rule);
font: 500 15px/1.5 var(--font-mono);
letter-spacing: .01em;
color: var(--paper-dim);
}
.admin-sheet .tbl .thead > *:last-child,
.admin-sheet .tbl .trow > *:last-child {
padding-right: 0;
}
.admin-sheet .tbl .c-title,
.admin-sheet .tbl .c-site {
font: 400 18px/1.35 var(--font-display);
letter-spacing: 0;
color: var(--paper);
}
.admin-sheet .tbl .c-title a:hover,
.admin-sheet .tbl .c-act .ghost:hover {
color: var(--patina);
}
.admin-sheet .tbl .trow.attention .c-title {
color: var(--patina);
}
.admin-sheet .tbl .c-ch,
.admin-sheet .tbl .c-rd {
text-align: right;
padding-right: 26px;
}
.admin-sheet .tbl .trow .c-note .mark {
margin-right: 8px;
}
.admin-sheet .tbl .c-act {
text-align: right;
}
.admin-sheet .tbl .c-act .ghost + .ghost {
margin-left: 12px;
}
.admin-sheet .tbl .trow > .confirm-row {
grid-column: 1 / -1;
padding: 10px 12px;
border-bottom: none;
}
.admin-sheet .stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
margin: 4px 0 0;
border-bottom: 1px solid var(--rule);
}
.admin-sheet .stat {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
padding: 11px 24px 11px 0;
}
.admin-sheet .stat .lbl {
font: 500 13px/1.3 var(--font-mono);
letter-spacing: .1em;
text-transform: uppercase;
color: var(--mute-2);
}
.admin-sheet .stat .fig {
font: 500 19px/1 var(--font-mono);
font-variant-numeric: tabular-nums;
color: var(--paper);
}
.admin-sheet .stat .fig.zero {
color: var(--mute);
}
.admin-sheet .stat a.fig {
/* The hover rule below sets the border on :hover only, which shifts the
row a pixel on hover; the transparent baseline holds the layout. */
border-bottom: 1px solid transparent;
}
.admin-sheet .stat a.fig:hover {
color: var(--patina);
border-bottom: 1px solid var(--patina);
}
.admin-sheet .mark,
.admin-sheet .mark-faint {
font: 500 13px/1 var(--font-mono);
letter-spacing: .06em;
text-transform: uppercase;
white-space: nowrap;
}
.admin-sheet .mark {
color: var(--patina);
}
.admin-sheet .mark-faint {
color: var(--mute-2);
}
.admin-sheet .mark.mark-strong {
font-size: 13px;
letter-spacing: .14em;
color: var(--patina);
}
.admin-sheet .mark.mark-strong::before {
content: "";
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--patina);
margin-right: 8px;
vertical-align: .08em;
}
.admin-sheet .mark.bad {
color: var(--danger);
}
.admin-sheet .tbl.sites {
grid-template-columns: 170px repeat(4, 106px) minmax(180px, 1fr);
}
.admin-sheet .tbl.sites .c-site,
.admin-sheet .tbl.lanes .c-site {
font: 400 18px/1.35 var(--font-display);
letter-spacing: 0;
color: var(--paper);
}
.admin-sheet .tbl.sites .c-state {
color: var(--patina);
white-space: nowrap;
}
.admin-sheet .tbl.sites .c-state.bad,
.admin-sheet .tbl.lanes .c-skip .bad,
.admin-sheet .tbl.lanes .trow.attention .c-site {
color: var(--danger);
}
.admin-sheet .tbl.lanes .c-skip .ok {
color: var(--patina);
}
.admin-sheet .tbl.sites .thead > *:nth-child(n+2):nth-child(-n+5),
.admin-sheet .tbl.sites .trow > *:nth-child(n+2):nth-child(-n+5) {
padding-right: 0;
text-align: center;
}
.admin-sheet .tbl.lanes {
grid-template-columns: 150px 62px 88px 66px 118px minmax(0, 1fr) 168px;
}
.admin-sheet .tbl.lanes .c-skip {
white-space: normal;
}
.admin-sheet .tbl.lanes .c-skip > * {
white-space: nowrap;
}
.admin-sheet .tbl.lanes .c-ctrl {
text-align: right;
}
.admin-sheet .tbl.lanes .thead > *:nth-child(n+2):nth-child(-n+4),
.admin-sheet .tbl.lanes .trow > *:nth-child(n+2):nth-child(-n+4) {
padding-right: 26px;
text-align: right;
}
.admin-sheet .tbl.series {
grid-template-columns: 150px 84px 104px 76px minmax(120px, 1fr) 212px;
row-gap: 4px;
}
.admin-sheet .tbl.series .thead > *:first-child {
padding-left: 20px;
}
.admin-sheet .tbl.series .thead > *:last-child {
padding-right: 20px;
}
/* The site cell's colour is a class, never an inline style: site is
client-supplied and unvalidated, and a hostile value reaching a CSS
context would render ZgotmplZ, while an unknown class degrades to the
unstyled cell. */
.admin-sheet .tbl .c-site.site-asura {
color: var(--asura);
}
.admin-sheet .tbl .c-site.site-demonic {
color: var(--demonic);
}
.admin-sheet .tbl .c-site.site-comix {
color: var(--comix);
}
.admin-sheet .tbl .c-site.site-kagane {
color: var(--kagane);
}
.admin-sheet .tbl .c-site.site-novelfull {
color: var(--novelfull);
}
.admin-sheet .tbl .c-site.site-lightnovelworld {
color: var(--lightnovelworld);
}
.admin-sheet .tbl.series .trow {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
padding: 13px 20px 14px;
}
.admin-sheet .tbl.series .trow.band {
background: var(--hover);
}
.admin-sheet .tbl.series .trow > * {
padding: 0 14px 0 0;
border-bottom: none;
}
.admin-sheet .tbl.series .c-title {
display: flex;
grid-column: 1 / -1;
align-items: baseline;
gap: 16px;
padding: 0 0 4px;
}
.admin-sheet .tbl.series .c-title .mark {
margin-left: auto;
}
.admin-sheet .filterbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px 22px;
padding: 16px 0 14px;
border-bottom: 1px solid var(--rule);
}
.admin-sheet .fsel {
display: flex;
align-items: baseline;
gap: 10px;
}
.admin-sheet .fsel > span {
font: 500 11px/1 var(--font-mono);
letter-spacing: .14em;
text-transform: uppercase;
color: var(--mute-2);
}
.admin-sheet .fsel select,
.admin-sheet .pausebar select {
color-scheme: dark;
background: var(--ink);
border: none;
border-bottom: 1px solid var(--field-line);
color: var(--paper);
font: 400 16px/1.4 var(--font-display);
padding: 4px 4px 5px 0;
}
.admin-sheet .fsel select:hover,
.admin-sheet .pausebar select:hover {
border-bottom-color: var(--patina);
}
.admin-sheet .fsel select option,
.admin-sheet .pausebar select option {
background: var(--ink);
color: var(--paper);
}
.admin-sheet .segrow {
display: inline-flex;
gap: 2px;
}
.admin-sheet .segrow a {
padding: 6px 11px 7px;
border-bottom: 2px solid transparent;
color: var(--mute);
font: 500 14px/1 var(--font-mono);
}
.admin-sheet .segrow a.active {
border-bottom-color: var(--patina);
color: var(--patina);
}
.admin-sheet .listhead {
display: flex;
align-items: baseline;
gap: 12px;
padding: 16px 0 4px;
color: var(--paper);
font: 400 18px/1.2 var(--font-display);
}
.admin-sheet .listhead .lbl {
color: var(--mute);
}
.admin-sheet .listhead .lbl em {
color: var(--patina);
font-style: normal;
}
.admin-sheet .pager {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 0 0;
color: var(--mute-2);
font: 500 11px/1 var(--font-mono);
letter-spacing: .14em;
text-transform: uppercase;
}
.admin-sheet .pager .pg {
color: var(--paper-dim);
}
.admin-sheet .pager .pg:hover {
color: var(--paper);
}
.admin-sheet .pager .pg.disabled {
color: var(--faint);
pointer-events: none;
}
.admin-sheet .empty {
padding: 28px 0;
text-align: center;
}
.admin-sheet .empty strong {
color: var(--paper);
font: 400 20px var(--font-display);
}
.admin-sheet .empty p {
margin: 6px 0 0;
font: 14px/1.5 var(--font-body);
color: var(--mute);
}
.admin-sheet .confirm-row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: var(--danger-wash);
}
.admin-sheet .confirm-row span {
flex: 1 1 16ch;
color: var(--danger-soft);
font: 400 15px/1.3 var(--font-display);
}
.admin-sheet .confirm-row div {
display: flex;
flex: none;
gap: 12px;
margin-left: auto;
}
.admin-sheet .tbl.series .row-msg {
grid-column: 1 / -1;
margin-top: 6px;
color: var(--danger-soft);
font: 400 13px/1.4 var(--font-body);
}
.admin-sheet .detail-back {
display: inline-block;
margin: 18px 0 0;
}
.admin-sheet .detail-title {
margin: 10px 0 2px;
color: var(--paper);
font: 400 28px/1.25 var(--font-display);
}
.admin-sheet .detail-key {
margin: 0;
color: var(--mute-2);
font: 500 11px/1.4 var(--font-mono);
letter-spacing: .08em;
}
.admin-sheet .detail-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 14px;
margin: 10px 0 0;
color: var(--mute-2);
font: 500 12px/1.5 var(--font-mono);
letter-spacing: .08em;
text-transform: uppercase;
}
.admin-sheet .cover {
width: 160px;
aspect-ratio: 3 / 4;
display: flex;
align-items: center;
justify-content: center;
margin: 18px 0 4px;
background: var(--hatch);
color: var(--mute-2);
font: 500 10px/1 var(--font-mono);
letter-spacing: .2em;
text-transform: uppercase;
}
.admin-sheet .detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 28px;
}
.admin-sheet .dform {
padding: 14px 0 0;
}
.admin-sheet .dform h3 {
margin: 0 0 4px;
color: var(--mute-2);
font: 500 10px/1 var(--font-mono);
letter-spacing: .2em;
text-transform: uppercase;
}
.admin-sheet .dform .field {
display: flex;
gap: 10px;
margin-top: 8px;
}
.admin-sheet .dform input {
min-width: 0;
padding: 8px 10px;
border: 1px solid var(--field-line);
background: var(--ink);
color: var(--paper);
font: 500 14px var(--font-mono);
outline: none;
}
/* Focus follows the chapter form's idiom — paper, not heat: a red border on
a valid number field reads as "invalid". */
.admin-sheet .dform input:focus {
border-color: var(--paper);
}
.admin-sheet .dform .hint {
margin: 0;
color: var(--mute-2);
font: 500 12px/1.4 var(--font-mono);
letter-spacing: .04em;
}
.admin-sheet .pausebar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
@media (max-width: 719px) {
.admin-sheet .topbar {
flex-wrap: wrap;
row-gap: 12px;
}
.admin-sheet .brand {
flex: 1 1 100%;
}
.admin-sheet .topbar-actions {
margin-left: auto;
}
}
@media (max-width: 1019px) {
.admin-sheet .tbl.lanes {
display: block;
}
.admin-sheet .tbl.lanes .thead {
display: none;
}
.admin-sheet .tbl.lanes .trow {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 16px;
padding: 12px 0;
border-bottom: 1px solid var(--rule);
}
.admin-sheet .tbl.lanes .trow > * {
padding: 0;
border-bottom: none;
text-align: left;
}
.admin-sheet .tbl.lanes .c-site {
width: 100%;
padding-bottom: 2px;
}
.admin-sheet .tbl.lanes .c-ctrl {
margin-left: auto;
text-align: right;
}
}
@media (max-width: 899px) {
.admin-sheet .tbl,
.admin-sheet .tbl.lanes {
display: block;
}
.admin-sheet .tbl .thead {
display: none;
}
.admin-sheet .tbl .trow {
display: flex;
flex-wrap: wrap;
gap: 2px 10px;
padding: 11px 0;
border-bottom: 1px solid var(--rule);
}
.admin-sheet .tbl .trow > * {
padding: 0;
border-bottom: none;
text-align: left;
}
.admin-sheet .tbl .c-ch,
.admin-sheet .tbl .c-rd {
padding-right: 0;
text-align: left;
}
.admin-sheet .tbl .c-title {
width: 100%;
padding-bottom: 2px;
}
.admin-sheet .tbl.series .trow {
display: flex;
}
.admin-sheet .tbl.series .trow > * {
padding: 0;
}
.admin-sheet .detail-grid {
grid-template-columns: 1fr;
}
.admin-sheet .tbl .c-act {
margin-left: auto;
}
}