/* liseur-sync web UI (ADR-0011).
 *
 * One stylesheet, no build step, no framework. It is ordered tokens →
 * reset → shell → components → pages, and every declaration lives here:
 * no markup carries a style attribute, so a Content-Security-Policy for
 * /ui stays a decision rather than a migration.
 */

/* ---------------------------------------------------------------- tokens */

:root {
	color-scheme: dark;

	--bg: #121215;
	--surface: #1b1b20;
	--surface-2: #24242b;
	--surface-3: #2e2e37;
	--line: #33333d;
	--line-soft: #26262e;
	--ink: #e8e8ec;
	--ink-dim: #b7b7c2;
	--muted: #8b8b98;
	--primary: #5b8cf5;
	--primary-dim: #3a63c0;
	--on-primary: #0b1020;
	--danger: #f06a6a;
	--ok: #62c184;
	--warn: #e0b252;

	/* Heatmap ramp, dimmest to brightest. */
	--c1: #1f3a2a; --c2: #2f6b45; --c3: #46a56a; --c4: #7fe0a4;

	--rail: 15rem;
	--gap: 1rem;
	--radius: .7rem;
	--radius-sm: .4rem;
	--shadow: 0 1px 2px rgb(0 0 0 / .4), 0 6px 18px rgb(0 0 0 / .28);
	--shadow-sm: 0 1px 2px rgb(0 0 0 / .35);

	/* A cover is taller than it is wide, and a grid that assumes one
	   ratio is calmer than a grid that lets every publisher choose. */
	--cover-ratio: 2 / 3;
	--card-min: 11.5rem;
}

[data-theme="light"] {
	color-scheme: light;

	--bg: #f4f5f7;
	--surface: #fff;
	--surface-2: #f0f1f4;
	--surface-3: #e5e7ec;
	--line: #dcdee5;
	--line-soft: #e9ebf0;
	--ink: #1a1a1f;
	--ink-dim: #3c3c46;
	--muted: #64646f;
	--primary: #2d5fd0;
	--primary-dim: #244ba6;
	--on-primary: #fff;
	--danger: #c22c2c;
	--ok: #1c7a45;
	--warn: #97650d;

	--c1: #d9f2e2; --c2: #8fd7ac; --c3: #46a56a; --c4: #1c7a45;

	--shadow: 0 1px 2px rgb(16 20 30 / .08), 0 6px 18px rgb(16 20 30 / .08);
	--shadow-sm: 0 1px 2px rgb(16 20 30 / .1);
}

[data-theme="tokyo-night"] {
	color-scheme: dark;

	--bg: #1a1b26;
	--surface: #16161e;
	--surface-2: #1f2335;
	--surface-3: #292e42;
	--line: #3b4261;
	--line-soft: #292e42;
	--ink: #c0caf5;
	--ink-dim: #a9b1d6;
	--muted: #565f89;
	--primary: #7aa2f7;
	--primary-dim: #3d59a1;
	--on-primary: #1a1b26;
	--danger: #f7768e;
	--ok: #73daca;
	--warn: #e0af68;

	--c1: #1f2a44; --c2: #2d4f7a; --c3: #3d7fb3; --c4: #7dcfff;
}

[data-theme="rose-pine"] {
	color-scheme: dark;

	--bg: #191724;
	--surface: #1f1d2e;
	--surface-2: #26233a;
	--surface-3: #403d52;
	--line: #524f67;
	--line-soft: #26233a;
	--ink: #e0def4;
	--ink-dim: #908caa;
	--muted: #6e6a86;
	--primary: #c4a7e7;
	--primary-dim: #907aa9;
	--on-primary: #191724;
	--danger: #eb6f92;
	--ok: #9ccfd8;
	--warn: #f6c177;

	--c1: #213b44; --c2: #2f6172; --c3: #6e9ca8; --c4: #9ccfd8;
}

/* "system" defers to the browser: the dark tokens above are already the
   default, so only the light case needs stating. */
@media (prefers-color-scheme: light) {
	[data-theme="system"] {
		color-scheme: light;

		--bg: #f4f5f7;
		--surface: #fff;
		--surface-2: #f0f1f4;
		--surface-3: #e5e7ec;
		--line: #dcdee5;
		--line-soft: #e9ebf0;
		--ink: #1a1a1f;
		--ink-dim: #3c3c46;
		--muted: #64646f;
		--primary: #2d5fd0;
		--primary-dim: #244ba6;
		--on-primary: #fff;
		--danger: #c22c2c;
		--ok: #1c7a45;
		--warn: #97650d;

		--c1: #d9f2e2; --c2: #8fd7ac; --c3: #46a56a; --c4: #1c7a45;

		--shadow: 0 1px 2px rgb(16 20 30 / .08), 0 6px 18px rgb(16 20 30 / .08);
		--shadow-sm: 0 1px 2px rgb(16 20 30 / .1);
	}
}

/* ----------------------------------------------------------------- reset */

* { box-sizing: border-box; }

body {
	margin: 0;
	background: var(--bg);
	color: var(--ink);
	font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
	font-size: 15px;
	line-height: 1.55;
	-webkit-text-size-adjust: 100%;
}

a { color: var(--primary); }
a:hover { color: var(--ink); }

h1 { font-size: 1.45rem; margin: 0 0 .6rem; letter-spacing: -.01em; }
h2 { font-size: 1.05rem; margin: 0 0 .5rem; }
h3 { font-size: .95rem; margin: 0 0 .4rem; }

:focus-visible {
	outline: 2px solid var(--primary);
	outline-offset: 2px;
	border-radius: 2px;
}

@media (prefers-reduced-motion: reduce) {
	* { transition: none !important; animation: none !important; }
}

.visually-hidden {
	position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
	overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip {
	position: absolute; left: .5rem; top: -3rem; z-index: 60;
	background: var(--primary); color: var(--on-primary);
	padding: .5rem .9rem; border-radius: var(--radius-sm);
	text-decoration: none; transition: top .15s;
}
.skip:focus { top: .5rem; }

/* ----------------------------------------------------------------- shell */

.app {
	display: grid;
	grid-template-areas: "top top" "rail main";
	grid-template-columns: var(--rail) 1fr;
	grid-template-rows: auto 1fr;
	min-height: 100vh;
}

.topbar {
	grid-area: top;
	display: flex; align-items: center; gap: 1rem;
	padding: .55rem .9rem;
	background: var(--surface);
	border-bottom: 1px solid var(--line);
	position: sticky; top: 0; z-index: 30;
}

.brand {
	font-weight: 700; letter-spacing: -.02em;
	color: var(--ink); text-decoration: none;
	display: flex; align-items: center; gap: .5rem;
}
.brand:hover { color: var(--primary); }
/* The Liseur wordmark, cut out of the banner the app's README wears, so
   the server and the reader that syncs to it look like one thing. It is
   the lettering alone: the banner's drawing is fine line art and turns
   to mush at the height of a top bar. It is a background rather than an
   <img> because it has to follow the theme — the lettering is cream on
   one banner and ink on the other, and a src cannot be chosen in CSS.
   The element carries the name as an aria-label, the picture being the
   wordmark. */
.brand .brandmark {
	display: block; flex: none;
	height: 1.55rem; width: 7.5rem;
	background: url("brand-dark.png") left center / contain no-repeat;
}
[data-theme="light"] .brand .brandmark { background-image: url("brand-light.png"); }
@media (prefers-color-scheme: light) {
	[data-theme="system"] .brand .brandmark { background-image: url("brand-light.png"); }
}
.brand .suffix {
	font-size: .8rem; font-weight: 600; color: var(--muted);
	letter-spacing: .06em; text-transform: lowercase;
}
@media (max-width: 32em) { .brand .suffix { display: none; } }

.account { margin-left: auto; display: flex; align-items: center; gap: .5rem; }
.account .who { color: var(--muted); font-size: .85rem; }
.account form { margin: 0; }

.rail {
	grid-area: rail;
	background: var(--surface);
	border-right: 1px solid var(--line);
	padding: .8rem .6rem;
	position: sticky; top: 3.1rem;
	align-self: start;
	max-height: calc(100vh - 3.1rem);
	overflow-y: auto;
}
.rail nav { display: flex; flex-direction: column; gap: .1rem; }
.rail a {
	display: flex; align-items: center; gap: .6rem;
	padding: .5rem .7rem;
	border-radius: var(--radius-sm);
	color: var(--ink-dim); text-decoration: none;
	font-size: .92rem;
}
.rail a:hover { background: var(--surface-2); color: var(--ink); }
.rail a[aria-current="page"] {
	background: var(--surface-3); color: var(--ink); font-weight: 600;
}
main { grid-area: main; padding: 1.4rem 1.5rem 4rem; min-width: 0; }
.page { max-width: 78rem; margin: 0 auto; }
.foot { color: var(--muted); font-size: .8rem; text-align: center; padding: 2rem 0 0; }

/* The drawer. A checkbox rather than a script, so navigation on a phone
   does not depend on JavaScript any more than the rest of this UI does. */
.nav-toggle { display: none; }
.nav-open, .nav-scrim { display: none; }

@media (max-width: 60em) {
	.app { grid-template-areas: "top" "main"; grid-template-columns: 1fr; }
	.nav-open {
		display: inline-flex; align-items: center; justify-content: center;
		width: 2.2rem; height: 2.2rem; border-radius: var(--radius-sm);
		border: 1px solid var(--line); background: var(--surface-2);
		color: var(--ink); cursor: pointer;
	}
	.rail {
		position: fixed; inset: 0 auto 0 0; width: min(17rem, 82vw);
		max-height: none; z-index: 50; transform: translateX(-102%);
		transition: transform .18s ease; box-shadow: var(--shadow);
	}
	.nav-toggle:checked ~ .app .rail { transform: none; }
	.nav-toggle:checked ~ .app .nav-scrim {
		display: block; position: fixed; inset: 0; z-index: 40;
		background: rgb(0 0 0 / .5);
	}
	main { padding: 1rem .9rem 3rem; }
}

/* ------------------------------------------------------------ components */

.card {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	padding: 1.1rem 1.2rem;
	margin: 0 0 var(--gap);
	box-shadow: var(--shadow-sm);
}
details.card > summary {
	cursor: pointer; font-weight: 600; color: var(--ink);
	list-style: none; display: flex; align-items: center; gap: .5rem;
}
details.card > summary::before { content: "+"; color: var(--muted); }
details.card[open] > summary::before { content: "−"; }

/* A disclosure inside a form (the root-library form's advanced axes):
   quieter than a card summary, since the action below it is the point. */
details.advanced { margin: 0 0 .8rem; }
details.advanced > summary {
	cursor: pointer; color: var(--muted); list-style: none;
	display: flex; align-items: center; gap: .5rem; font-size: .95rem;
}
details.advanced > summary::before { content: "+"; }
details.advanced[open] > summary::before { content: "−"; }

/* The add-library form: the folder fields only matter once "a folder on
   this server" is picked. Hidden with :has() so it needs no script; a
   browser without :has() shows the whole form, which still works. */
.addlib .choice { border: 0; padding: 0; margin: 0 0 .8rem; }
.addlib .choice legend { font-weight: 600; padding: 0 0 .3rem; }
.addlib:has(input[name="from"][value="uploads"]:checked) .folder-only {
	display: none;
}

.narrow { max-width: 23rem; margin: 4rem auto; }

/* Settings is the single account hub. The two-level tab treatment keeps
   profile, devices and admin work distinct without adding another rail
   entry or requiring JavaScript. */
.settings-heading { margin: 0 0 .9rem; }
.settings-heading p { margin: 0; max-width: 42rem; }
.settings-tabs, .settings-subnav {
	display: flex; flex-wrap: wrap; gap: .35rem;
	margin: 0 0 var(--gap); padding: .35rem;
	border: 1px solid var(--line);
	border-radius: var(--radius);
	background: var(--surface);
}
.settings-tabs a, .settings-subnav a {
	padding: .42rem .8rem;
	border-radius: var(--radius-sm);
	color: var(--ink-dim);
	text-decoration: none;
	font-size: .9rem;
	white-space: nowrap;
}
.settings-tabs a:hover, .settings-subnav a:hover {
	background: var(--surface-3);
	color: var(--ink);
}
.settings-tabs a[aria-current="page"] {
	background: var(--primary);
	color: var(--on-primary);
	font-weight: 600;
}
.settings-subnav {
	margin-top: -.25rem;
	background: var(--surface-2);
	border-color: var(--line-soft);
}
.settings-subnav a[aria-current="page"] {
	background: var(--surface-3);
	color: var(--ink);
	font-weight: 600;
}
.settings-card > form { max-width: 48rem; }

/* The admin section's own navigation: one rail entry, several pages. */
.subnav {
	display: flex; flex-wrap: wrap; gap: .3rem;
	margin: 0 0 var(--gap); padding-bottom: .5rem;
	border-bottom: 1px solid var(--line);
}
.subnav a {
	padding: .3rem .7rem; border-radius: 2rem; text-decoration: none;
	color: var(--ink-dim); font-size: .9rem;
}
.subnav a:hover { background: var(--surface-3); color: var(--ink); }
.subnav a[aria-current="page"] {
	background: var(--primary); color: var(--on-primary);
}

/* A labelled list of read-only facts: config, build, counts. */
dl.facts {
	display: grid; grid-template-columns: minmax(10rem, auto) 1fr;
	gap: .35rem 1rem; margin: 0;
}
dl.facts dt { color: var(--muted); font-size: .85rem; }
dl.facts dd { margin: 0; }
@media (max-width: 30rem) {
	dl.facts { grid-template-columns: 1fr; gap: 0 0; }
	dl.facts dd { margin: 0 0 .5rem; }
}

.cards { display: flex; gap: var(--gap); flex-wrap: wrap; margin: 0 0 var(--gap); }
.stat {
	background: var(--surface); border: 1px solid var(--line);
	border-radius: var(--radius); padding: .8rem 1.1rem; min-width: 8.5rem;
	display: flex; flex-direction: column;
}
.stat .num { font-size: 1.7rem; font-weight: 700; line-height: 1.15; }
.stat .lbl { color: var(--muted); font-size: .8rem; }

button, .btn {
	font: inherit; line-height: 1.4;
	padding: .38rem .9rem; border-radius: var(--radius-sm);
	border: 1px solid var(--line); background: var(--surface-2);
	color: var(--ink); cursor: pointer; text-decoration: none;
	display: inline-flex; align-items: center; gap: .4rem;
}
button:hover, .btn:hover { background: var(--surface-3); color: var(--ink); }
button.primary, .btn.primary {
	background: var(--primary); border-color: var(--primary); color: var(--on-primary);
	font-weight: 600;
}
button.primary:hover, .btn.primary:hover { background: var(--primary-dim); color: var(--on-primary); }
button.danger { color: var(--danger); border-color: transparent; background: none; }
button.danger:hover { background: color-mix(in srgb, var(--danger) 15%, transparent); }
button.link { background: none; border: none; color: var(--muted); padding: .2rem; }
button.link:hover { color: var(--ink); background: none; }
button[disabled] { opacity: .5; cursor: not-allowed; }

table { border-collapse: collapse; width: 100%; margin: .4rem 0; font-size: .92rem; }
th, td { text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--line-soft); }
th { color: var(--muted); font-weight: 600; font-size: .78rem;
	text-transform: uppercase; letter-spacing: .05em; }
tbody tr:hover { background: var(--surface-2); }
tr:last-child td { border-bottom: 0; }

.session-device { width: 10rem; max-width: 10rem; }
.session-device .device-name,
.session-device .device-id {
	display: block; max-width: 100%; overflow: hidden;
	text-overflow: ellipsis; white-space: nowrap;
}
.session-device .device-name { font-weight: 600; }
.session-device .device-id {
	width: 100%; color: var(--muted); font-size: .76em;
}

code {
	background: var(--surface-2); border: 1px solid var(--line-soft);
	padding: .1em .35em; border-radius: var(--radius-sm);
	font-size: .88em;
}
code.big { font-size: 1rem; display: inline-block; padding: .45rem .6rem; word-break: break-all; }

/* A field is a stack: label, control, then whatever the server has to
   say about it. Left to themselves these run into each other, because a
   textarea and its hint are siblings. */
.card form p { display: flex; flex-direction: column; gap: .25rem; max-width: 40rem; }
.card form p > label { margin: 0; }
.card form p > .muted { font-size: .8rem; }
.card form p > input, .card form p > textarea { width: 100%; }

form.inline { display: flex; gap: .6rem; align-items: center; margin-top: .7rem; flex-wrap: wrap; }
form label { display: block; margin: .6rem 0; color: var(--ink-dim); font-size: .9rem; }
form label.check { display: flex; gap: .5rem; align-items: center; }
.narrow form > label {
	display: grid; grid-template-columns: max-content minmax(0, 1fr);
	align-items: center; column-gap: .6rem; text-align: right;
}
.narrow form > label > input { width: 100%; min-width: 0; }
input, select, textarea {
	font: inherit; padding: .4rem .55rem;
	background: var(--surface-2); color: var(--ink);
	border: 1px solid var(--line); border-radius: var(--radius-sm);
}
input:hover, select:hover { border-color: var(--surface-3); }
fieldset { border: 1px solid var(--line); border-radius: var(--radius-sm); margin: .8rem 0; }
legend { color: var(--muted); font-size: .8rem; padding: 0 .4rem; }

.error { color: var(--danger); }
.ok, .notice { color: var(--ok); }
.muted { color: var(--muted); }

.tag, .chip {
	display: inline-flex; align-items: center; gap: .3rem;
	font-size: .78rem; background: var(--surface-3); color: var(--ink-dim);
	border: 1px solid var(--line); border-radius: 2rem;
	padding: .1rem .6rem; text-decoration: none;
}
a.chip:hover { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
.chip.on { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
.chips { display: flex; flex-wrap: wrap; gap: .35rem; margin: .4rem 0; }
.secret { border-color: var(--primary); }

.bar {
	display: inline-block; width: 7em; height: .5rem; vertical-align: middle;
	background: var(--surface-3); border-radius: 1rem; overflow: hidden; margin-right: .5rem;
}
.bar span { display: block; height: 100%; background: var(--primary); }

/* --------------------------------------------------- dashboard insights */

.dashhead {
	display: flex; align-items: end; justify-content: space-between;
	gap: 1rem; margin: 0 0 .75rem; flex-wrap: wrap;
}
.dashhead h1 { font-size: 2rem; line-height: 1.1; margin: 0; }
.dashhead .eyebrow { margin: 0 0 .15rem; }
.spanpick { margin: 0; }
.spanpick select { min-width: min(12rem, 44vw); }

/* One bar a day, for the spans short enough to read that way. The row
   stretches, so a week's seven bars are wide and a month's thirty-one
   are narrow, and neither leaves a gap at the end. */
.daybars {
	display: flex; align-items: flex-end; gap: 3px;
	margin: .5rem 0 0;
}
.daybar { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: .35rem; }
/* No track behind the bar. A full-height box on a day with no reading
   in it reads as a bar in its own right, and the chart then says the
   opposite of what happened; an empty day is a stub on the baseline. */
.daybar-track { height: 6rem; display: flex; align-items: flex-end; }
/* The height classes are the progress bars' percentage steps (.p0-.p100)
   turned on their side; the CSP has no unsafe-inline, so a bar's height
   has to be a class like every other measurement here. */
.daybar-track > span {
	width: 100%; align-self: flex-end;
	border-radius: 3px 3px 0 0;
}
.daybar-track > .c0 { height: 2px; background: var(--surface-3) }
.daybar-track > .p5 { height: 5% }
.daybar-track > .p10 { height: 10% } .daybar-track > .p15 { height: 15% }
.daybar-track > .p20 { height: 20% } .daybar-track > .p25 { height: 25% }
.daybar-track > .p30 { height: 30% } .daybar-track > .p35 { height: 35% }
.daybar-track > .p40 { height: 40% } .daybar-track > .p45 { height: 45% }
.daybar-track > .p50 { height: 50% } .daybar-track > .p55 { height: 55% }
.daybar-track > .p60 { height: 60% } .daybar-track > .p65 { height: 65% }
.daybar-track > .p70 { height: 70% } .daybar-track > .p75 { height: 75% }
.daybar-track > .p80 { height: 80% } .daybar-track > .p85 { height: 85% }
.daybar-track > .p90 { height: 90% } .daybar-track > .p95 { height: 95% }
.daybar-track > .p100 { height: 100% }
.daybar-cap {
	font-size: .65rem; color: var(--muted); text-align: center;
	white-space: nowrap; overflow: hidden; min-height: 1em;
}

/* Weeks run down the columns and weekdays across the rows, so the grid
   reads like a wall calendar. It scrolls rather than shrinking: a year
   of 11px squares is wider than a phone, and squeezing them would cost
   the one thing the picture has, which is that a square is a day. */
.heatwrap { overflow-x: auto; margin: .5rem 0 0; padding-bottom: .25rem; }
.heatmap { display: flex; gap: 3px; width: max-content; }
.heatweek { width: 11px; display: flex; flex-direction: column; gap: 3px; }
.heatdays { display: grid; grid-template-rows: repeat(7, 11px); gap: 3px; }
/* The label is wider than the column it names and is allowed to say so:
   it runs on over the columns to its right, which are the rest of that
   same month. */
.heatmonth {
	font-size: .65rem; line-height: 1; height: 1em;
	color: var(--muted); white-space: nowrap;
}
.cell { width: 11px; height: 11px; border-radius: 2px; background: var(--surface-3); }
.cell.blank { background: none; }
.cell.c1 { background: var(--c1); } .cell.c2 { background: var(--c2); }
.cell.c3 { background: var(--c3); } .cell.c4 { background: var(--c4); }

/* -------------------------------------------------------- browse toolbar */

.toolbar {
	display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
	margin: 0 0 var(--gap);
}
.toolbar .spacer { flex: 1; }
.toolbar form { margin: 0; display: flex; gap: .4rem; align-items: center; }
.viewtoggle { display: inline-flex; }
.viewtoggle button, .viewtoggle .btn { border-radius: 0; }
.viewtoggle :first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.viewtoggle :last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.viewtoggle .on { background: var(--surface-3); color: var(--ink); font-weight: 600; }

.libraryhead {
	display: flex; align-items: end; justify-content: space-between;
	gap: 1rem; margin: 0 0 .75rem;
}
.libraryhead h1 { font-size: 2rem; line-height: 1.1; margin: 0; }
.libraryhead .eyebrow { margin: 0 0 .15rem; }
.folderpick { margin: 0; }
.folderpick select { min-width: min(18rem, 52vw); }
.librarytools {
	display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
	padding: .65rem 0 1rem; margin: 0 0 1.2rem;
	border-bottom: 1px solid var(--line);
}
.librarytools form { margin: 0; display: flex; gap: .4rem; align-items: center; }
.librarytools form:first-child { flex: 1 1 18rem; }
.librarytools input[type="search"] { width: min(100%, 30rem); border-radius: 2rem; padding: .48rem .9rem; }
.librarytools .spacer { flex: 1; }
.libraryshelf { margin: 0 0 var(--gap); }

/* ------------------------------------------------------------- the grid */

.grid {
	display: grid; gap: 1.5rem 1.25rem;
	grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
	margin: 0 0 var(--gap);
}

.bookcard { display: flex; flex-direction: column; gap: .5rem; min-width: 0; }
.bookcard .art {
	position: relative; display: block;
	aspect-ratio: var(--cover-ratio);
	background: var(--surface-2);
	border: 1px solid var(--line);
	border-radius: var(--radius-sm);
	overflow: hidden;
	box-shadow: var(--shadow-sm);
}
.bookcard .art img {
	width: 100%; height: 100%; object-fit: cover; display: block;
	transition: transform .18s ease;
}
.bookcard:hover .art img { transform: scale(1.03); }

/* Hover actions sit over the art and are revealed on hover or focus, so
   a keyboard reaches them exactly where a pointer does. */
.bookcard .actions {
	position: absolute; inset: auto 0 0 0;
	display: flex; gap: .4rem; padding: .5rem;
	background: linear-gradient(to top, rgb(0 0 0 / .72), transparent);
	opacity: 0; transition: opacity .15s ease;
}
.bookcard:hover .actions, .bookcard .actions:focus-within { opacity: 1; }
.bookcard .actions .btn { padding: .25rem .6rem; font-size: .8rem; }

.bookcard .badge {
	position: absolute; top: .4rem; right: .4rem;
	background: var(--primary); color: var(--on-primary);
	font-size: .68rem; font-weight: 700; letter-spacing: .04em;
	padding: .1rem .45rem; border-radius: 1rem;
}
.bookcard .badge.done { background: var(--ok); }

.bookcard .title {
	font-size: .95rem; font-weight: 650; color: var(--ink);
	text-decoration: none; line-height: 1.3;
	display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
	overflow: hidden;
}
.bookcard .title:hover { color: var(--primary); }
.bookcard .by {
	font-size: .82rem; color: var(--muted);
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The sublinks at the foot of a card. The cover carries on reading and
   the title names the book; the ways to the numbers behind it come
   after both, small and quiet. */
.bookcard .sublinks {
	display: flex; flex-wrap: wrap; gap: .1rem .5rem;
	font-size: .74rem; margin-top: .15rem;
}
.bookcard .sublinks a { color: var(--muted); text-decoration: none; }
.bookcard .sublinks a:hover, .bookcard .sublinks a:focus-visible {
	color: var(--primary); text-decoration: underline;
}
.bookcard .sublinks .reading-status { display: inline; margin: 0; }

/* A delete that sits in a line of links has to look like one of them:
   the control is a form, because it writes, and a button that looked
   like a button would shout across a wall of quiet covers. */
.inline-delete { display: inline; }
button.linkish {
	background: none; border: none; padding: 0;
	font: inherit; color: var(--muted); cursor: pointer;
}
button.linkish.danger { border-color: transparent; }
button.linkish:hover, button.linkish:focus-visible {
	color: var(--danger); background: none; text-decoration: underline;
}
.rowlinks { display: flex; gap: .5rem; align-items: center; margin: .6rem 0 1rem; }
.cta .reading-status { margin: 0; }

/* Progress across the foot of the cover. The width is a class rather
   than a style attribute — see ADR-0011 — so the stylesheet keeps every
   declaration and a CSP for /ui stays possible. */
.bookcard .prog {
	position: absolute; inset: auto 0 0 0; height: 3px;
	background: rgb(0 0 0 / .45);
}
.bookcard .prog span { display: block; height: 100%; background: var(--primary); }
.p0 { width: 0 } .p5 { width: 5% } .p10 { width: 10% } .p15 { width: 15% }
.p20 { width: 20% } .p25 { width: 25% } .p30 { width: 30% } .p35 { width: 35% }
.p40 { width: 40% } .p45 { width: 45% } .p50 { width: 50% } .p55 { width: 55% }
.p60 { width: 60% } .p65 { width: 65% } .p70 { width: 70% } .p75 { width: 75% }
.p80 { width: 80% } .p85 { width: 85% } .p90 { width: 90% } .p95 { width: 95% }
.p100 { width: 100% }

/* A shelf is a grid that does not want to be tall: fewer, wider tiles. */
.shelf { grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); }

/* The endless-scroll sentinel: a plain link until htmx reaches it. */
.more { grid-column: 1 / -1; text-align: center; padding: .8rem 0; }

/* A work has no cover to show either, so its tile is made of its title
   and how far through it you are. */
.bookcard .art-text {
	display: flex; flex-direction: column; justify-content: center; gap: .3rem;
	padding: 1rem; text-align: center; text-decoration: none; color: var(--ink);
	background: linear-gradient(160deg, var(--surface-3), var(--surface-2));
}
.bookcard .art-text .name {
	font-weight: 600; font-size: .95rem; line-height: 1.3;
	display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
	overflow: hidden;
}
.bookcard .art-text .count { color: var(--muted); font-size: .8rem; }
.bookcard .art-text .big {
	margin-top: .4rem; font-size: 1.4rem; font-weight: 700; color: var(--primary);
}

/* An author, a series or a tag has no cover of its own, so it gets a
   tile that says what it is and how much of it there is. */
.entitycard .art {
	display: flex; flex-direction: column; align-items: center; justify-content: center;
	gap: .3rem; text-align: center; padding: .8rem;
	background: linear-gradient(145deg, var(--surface-2), var(--surface-3));
	color: var(--ink); text-decoration: none;
}
.entitycard .art .name { font-weight: 600; font-size: .95rem; }
.entitycard .art .count { color: var(--muted); font-size: .8rem; }

/* --------------------------------------------------------- book detail */

.hero {
	display: flex; gap: clamp(1.5rem, 4vw, 3rem); flex-wrap: wrap;
	align-items: flex-start; margin: .75rem 0 2.5rem;
}
.hero .art {
	flex: 0 0 17rem; max-width: 42%;
	border-radius: var(--radius); overflow: hidden;
	border: 1px solid var(--line); box-shadow: var(--shadow);
}
.hero .art img { width: 100%; height: auto; display: block; }
.hero .facts { flex: 1; min-width: min(100%, 18rem); max-width: 48rem; }
.hero .facts h1 { font-size: clamp(2rem, 4vw, 3.2rem); line-height: 1.06; margin-bottom: .35rem; }
.hero .byline { color: var(--muted); margin: 0 0 .8rem; }
.hero .cta { display: flex; gap: .5rem; flex-wrap: wrap; margin: .9rem 0; }
.reading-state { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; margin: .9rem 0 .3rem; }
.reading-state .bar { margin: 0; width: 10rem; }
.tag.done { background: color-mix(in srgb, var(--ok) 18%, var(--surface-3)); border-color: var(--ok); color: var(--ink); }
.bookrelations { display: grid; gap: .7rem; margin: 1.2rem 0; }
.bookrelations .chips { margin: .2rem 0 0; }
.bookorganization { margin-top: 1.5rem; }
.book-organization-section { padding: .9rem 0; border-top: 1px solid var(--line-soft); }
.book-organization-section:first-of-type { border-top: 0; padding-top: .4rem; }
.book-organization-section:last-child { padding-bottom: 0; }
.book-organization-section > h2 { margin-bottom: .45rem; }
.description { max-width: 68ch; }

@media (max-width: 38em) {
	.libraryhead { align-items: flex-start; flex-direction: column; }
	.folderpick, .folderpick select { width: 100%; }
	.librarytools { align-items: stretch; }
	.librarytools form:first-child, .librarytools input[type="search"] { width: 100%; }
	.librarytools .viewtoggle { margin-left: auto; }
	.hero { gap: 1.25rem; margin-top: 0; }
	.hero .art { flex-basis: 9rem; max-width: 38%; }
	.hero .facts { min-width: 0; }
	.hero .facts h1 { font-size: 2rem; }
	.reading-state .bar { width: 8rem; }
}

.kv { display: grid; grid-template-columns: max-content 1fr; gap: .3rem 1rem; font-size: .9rem; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; }

/* Covers in a table row, which the list view still uses. */
/* A token table has five columns and a form in one of them; below the
   drawer breakpoint it scrolls sideways rather than wrapping into an
   unreadable stack. */
@media (max-width: 46em) {
	.card > table { display: block; overflow-x: auto; }
	.settings-tabs, .settings-subnav {
		flex-wrap: nowrap;
		overflow-x: auto;
	}
}

.cover-thumb { width: 40px; height: 60px; object-fit: cover; border-radius: 2px; display: block; }

@media (max-width: 46em) {
	.hero .art { flex-basis: 100%; max-width: 12rem; }
	:root { --card-min: 7.5rem; }
}

.empty-state {
	text-align: center; padding: 3rem 1.5rem;
	background: var(--surface-1); border: 1px dashed var(--line);
	border-radius: var(--radius-md); margin: 1rem 0;
	display: flex; flex-direction: column; align-items: center; gap: .75rem;
}
.empty-state h3 { margin: 0; font-size: 1.1rem; color: var(--ink); }
.empty-state p { margin: 0; max-width: 26rem; font-size: .9rem; color: var(--muted); }

.dev-cell {
	display: inline-flex; align-items: center; gap: .5rem;
}
.dev-icon {
	display: inline-flex; align-items: center; justify-content: center;
	width: 1.6rem; height: 1.6rem; border-radius: var(--radius-sm);
	background: var(--surface-2); color: var(--muted); flex-shrink: 0;
}
.dev-web { color: var(--primary); background: var(--surface-1); }
.dev-mobile { color: var(--ok); background: var(--surface-1); }
.dev-ereader { color: var(--primary); background: var(--surface-1); opacity: .85; }
.dev-desktop { color: var(--ink); background: var(--surface-1); opacity: .75; }

/* The reader (ADR-0007): a book wants the window, so this page drops
   the usual chrome and gives the sandboxed frame everything left. */
.reader-body{margin:0;height:100vh;display:flex;flex-direction:column;overflow:hidden}
.reader-bar{display:flex;align-items:center;gap:1rem;padding:.5rem .9rem;
  border-bottom:1px solid var(--line);background:var(--surface)}
.reader-back{text-decoration:none;white-space:nowrap}
.reader-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.reader-title span{opacity:.7;margin-left:.5rem}
.reader-meta{display:flex;gap:.9rem;opacity:.75;font-size:.85rem;white-space:nowrap}
.reader-progress{height:3px;background:var(--surface-3)}
#reader-progress-bar{height:100%;width:0;background:var(--primary);transition:width .2s}
.reader-status{margin:0;padding:.5rem .9rem;font-size:.9rem;opacity:.8}
.reader-status.problem{color:var(--danger);opacity:1}
.reader-help{background:var(--surface);color:inherit;border:1px solid var(--line);
  border-radius:10px;box-shadow:0 12px 32px rgba(0,0,0,.45);padding:1.1rem 1.3rem;
  min-width:20rem;font-size:.9rem}
.reader-help::backdrop{background:rgba(0,0,0,.55)}
.reader-help h2{margin:0 0 .7rem;font-size:1rem}
.reader-help table{border-collapse:collapse}
.reader-help td{padding:.25rem .4rem;vertical-align:top}
.reader-help td:first-child{white-space:nowrap;padding-right:1.1rem}
.reader-help kbd{background:var(--surface-3);border:1px solid var(--line);
  border-radius:4px;padding:.05rem .35rem;font-size:.8rem;font-family:inherit}
.reader-help form{margin:.8rem 0 0;text-align:right}
.reader-toc-button{border:1px solid var(--line);border-radius:6px;background:transparent;
  color:inherit;cursor:pointer;padding:.1rem .55rem;font-size:1rem;line-height:1.4}
.reader-toc-button:hover{background:var(--surface-3)}
.reader-fullscreen-btn{border:1px solid var(--line);border-radius:6px;background:transparent;
  color:inherit;cursor:pointer;padding:.1rem .55rem;font-size:1rem;line-height:1.4;
  display:inline-flex;align-items:center}
.reader-fullscreen-btn:hover{background:var(--surface-3)}
.reader-toc{position:fixed;top:0;bottom:0;left:0;z-index:60;
  width:min(22rem,85vw);overflow-y:auto;padding:1rem 1.1rem;
  background:var(--surface);border-right:1px solid var(--line);
  box-shadow:0 0 32px rgba(0,0,0,.45);font-size:.9rem}
.reader-toc h2{margin:0 0 .6rem;font-size:1rem}
.reader-toc ul{list-style:none;margin:0;padding:0}
.reader-toc ul ul{padding-left:1rem}
.reader-toc li{margin:0}
.reader-toc a,.reader-toc span{display:block;padding:.3rem .45rem;border-radius:6px;
  color:inherit;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.reader-toc span{opacity:.65}
.reader-toc a:hover,.reader-toc a:focus-visible{background:var(--surface-3)}
.reader-toc a.current{background:var(--surface-3);color:var(--primary);font-weight:600}
.reader-annotations{margin-top:1rem;border-top:1px solid var(--line);padding-top:.75rem}
.reader-ann-kind{display:block;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;opacity:.65}
.reader-ann-text{display:block;font-size:.85rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.annlist{list-style:none;margin:0;padding:0}
.annlist li{padding:.6rem 0;border-bottom:1px solid var(--line)}
.annlist li:last-child{border-bottom:none}
.annlist .ann-kind{font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;opacity:.75;
  padding:.1rem .45rem;border-radius:999px;border:1px solid var(--line)}
.annlist .ann-excerpt{margin:.35rem 0;padding-left:.75rem;border-left:3px solid var(--line);font-style:italic}
.annlist .ann-body{margin:.35rem 0;white-space:pre-wrap}
.ann-color-yellow{border-left:4px solid #ffd54f}
.ann-color-green{border-left:4px solid #81c784}
.ann-color-blue{border-left:4px solid #64b5f6}
.ann-color-pink{border-left:4px solid #f06292}
.ann-color-purple{border-left:4px solid #ba68c8}
.ann-color-orange{border-left:4px solid #ffb74d}
.reader-stage{flex:1;display:flex;min-height:0}
#reader-view{flex:1;min-width:0;height:100%;background:#fff;overflow:hidden}
.reader-turn{border:0;background:transparent;cursor:pointer;font-size:2rem;
  padding:0 .6rem;opacity:.35;color:inherit}
.reader-turn:hover{opacity:.9}

/* Reader appearance settings (ADR-0012): a details popover, no script
   needed to open it and none to style it. The stage colour tracks the
   chosen theme so page gaps match the book instead of flashing white. */
.reader-settings{position:relative}
.reader-settings>summary{list-style:none;cursor:pointer;user-select:none;
  border:1px solid var(--line);border-radius:6px;padding:.1rem .55rem;
  font-weight:600;font-size:.9rem;line-height:1.4}
.reader-settings>summary::-webkit-details-marker{display:none}
.reader-settings>summary:hover,.reader-settings[open]>summary{background:var(--surface-3)}
.reader-settings>form{position:absolute;right:0;top:calc(100% + .55rem);z-index:40;
  width:19rem;max-height:min(75vh,34rem);overflow:auto;
  background:var(--surface);border:1px solid var(--line);border-radius:10px;
  box-shadow:0 12px 32px rgba(0,0,0,.45);padding:.9rem;
  display:grid;gap:.7rem;font-size:.85rem;white-space:normal}
.reader-settings fieldset{border:0;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:.35rem}
.reader-settings legend{width:100%;padding:0 0 .3rem;opacity:.65;
  font-size:.75rem;text-transform:uppercase;letter-spacing:.05em}
.reader-settings .chip{display:inline-flex;align-items:center;cursor:pointer;
  border:1px solid var(--line);border-radius:999px;padding:.2rem .65rem}
.reader-settings .chip input{position:absolute;opacity:0;pointer-events:none}
.reader-settings .chip:has(input:checked){border-color:var(--primary);
  background:var(--surface-3);color:var(--primary)}
.reader-settings .row{display:grid;grid-template-columns:auto 1fr auto;
  align-items:center;gap:.6rem}
.reader-settings .row select{justify-self:end}
.reader-settings .row2{display:flex;gap:.35rem}
.reader-settings input[type="range"]{width:100%}
.reader-settings output{opacity:.75;min-width:3ch;text-align:right}
#reader-settings-reset{border:1px solid var(--line);border-radius:6px;
  background:transparent;color:inherit;cursor:pointer;padding:.35rem .6rem}
#reader-settings-reset:hover{background:var(--surface-3)}
.reader-body[data-reader-theme="light"] #reader-view{background:#ffffff}
.reader-body[data-reader-theme="sepia"] #reader-view{background:#f6ecd9}
.reader-body[data-reader-theme="dark"] #reader-view{background:#202124}
.reader-body[data-reader-theme="tokyo-night"] #reader-view{background:#1a1b26}
.reader-body[data-reader-theme="rose-pine"] #reader-view{background:#191724}
.reader-body[data-reader-theme="black"] #reader-view{background:#000000}

/* htmx's request indicators. These are the rules htmx injects into the
   head at load; it is told not to (see the htmx-config meta in the
   layout) because a <style> element it wrote itself is exactly what a
   style-src the page can trust must refuse. Spelled here, they are
   served from this origin like everything else. */
.htmx-indicator { opacity: 0 }
.htmx-request .htmx-indicator { opacity: 1; transition: opacity 200ms ease-in }
.htmx-request.htmx-indicator { opacity: 1; transition: opacity 200ms ease-in }

/* ------------------------------------------------------------- library */

/* The continue-reading banner. It repeats a card that is also in the
   grid below, which is the point: coming back to a half-read book is
   the commonest reason to open this page. */
.resume {
	display: flex; gap: 1.25rem; align-items: flex-start;
	padding: .2rem 0 1.5rem; margin: 0 0 1.1rem;
	border-bottom: 1px solid var(--line);
}
.resume .art {
	flex: 0 0 7.5rem; display: block; overflow: hidden;
	border-radius: var(--radius-sm); border: 1px solid var(--line);
	box-shadow: var(--shadow-sm); aspect-ratio: var(--cover-ratio);
}
.resume .art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.resume .herotext { flex: 1; min-width: 12rem; display: flex; flex-direction: column; gap: .3rem; }
.eyebrow {
	font-size: .7rem; font-weight: 700; letter-spacing: .08em;
	text-transform: uppercase; color: var(--muted);
}
.resume .title { font-size: 1.35rem; font-weight: 650; color: var(--ink); text-decoration: none; }
.resume .title:hover { color: var(--primary); }
.resume .by { color: var(--muted); font-size: .85rem; }
.resume .bar { max-width: 22rem; }
.resume .rowlinks { margin: .5rem 0 0; }

@media (max-width: 32em) {
	.resume .art { flex-basis: 5.5rem; }
	.resume .title { font-size: 1.1rem; }
	.resume .rowlinks .btn:not(.primary) { display: none; }
}

/* The filter row under the banner. A chip is a link, so a filtered
   shelf has a URL somebody can send. */
.toolbar.filters {
	margin: .2rem 0 1.25rem; padding: .5rem 0;
	border-bottom: 1px solid var(--line-soft);
}
.sortchip { display: inline-flex; gap: .35rem; align-items: center; }
.groupseries label.check { margin: 0; white-space: nowrap; }
.groupseries input { accent-color: var(--primary); }

/* The open button on every cover.

   Always visible rather than revealed on hover, because hover does not
   exist on a phone and a web page cannot claim long-press — which is
   the whole reason this page needed designing rather than merging. It
   is a link to the book's page, not a menu: a popup anchored inside an
   overflow-hidden cover is clipped by the picture it hangs off, and
   every action it would have listed is on that page already. */
.cardopen {
	position: absolute; top: .3rem; right: .3rem; z-index: 2;
	display: flex; align-items: center; justify-content: center;
	width: 1.7rem; height: 1.7rem; border-radius: 50%;
	background: rgb(0 0 0 / .55); color: #fff;
	font-size: 1.1rem; line-height: 1; text-decoration: none;
	opacity: .6; transition: opacity .15s ease, background .15s ease;
}
.bookcard:hover .cardopen, .cardopen:focus-visible { opacity: 1; }
.cardopen:hover { opacity: 1; background: var(--primary); color: var(--on-primary); }

/* A text tile needs its link to fill the tile, since the menu sits on
   top of it and the two must not overlap. */
.bookcard .art-text .fill {
	display: flex; flex-direction: column; justify-content: center;
	gap: .3rem; height: 100%; text-decoration: none; color: inherit;
}
.bookcard .art-text .tag { align-self: center; margin-top: .4rem; }

/* A series occupies one grid cell, just like Android's mixed shelf. The
   front cover leaves a narrow edge at the right and foot for two real
   volumes to peek through, so the pile reads before its label does. */
.seriescard .seriesart {
	position: relative; display: block; aspect-ratio: .62;
	color: inherit; text-decoration: none;
}
.seriescard .stackback, .seriescard .stackfront {
	position: absolute; display: block; overflow: hidden;
	border: 1px solid var(--line); border-radius: var(--radius-sm);
	background: var(--surface-2); box-shadow: var(--shadow-sm);
}
.seriescard .stackback.far { inset: .7rem 0 0 .7rem; }
.seriescard .stackback.near { inset: .35rem .35rem .35rem .35rem; }
.seriescard .stackfront { inset: 0 .7rem .7rem 0; }
.seriescard .stackback img, .seriescard .stackfront img {
	width: 100%; height: 100%; object-fit: cover; display: block;
}
.seriescard .stackfront img { transition: transform .18s ease; }
.seriescard .stackback.far img { opacity: .45; }
.seriescard .stackback.near img { opacity: .65; }
.seriescard .stackfront.complete img { opacity: .55; }
.seriescard:hover .stackfront img { transform: scale(1.03); }
.seriescount {
	position: absolute; z-index: 2; top: .4rem; right: .9rem;
	display: flex; align-items: center; justify-content: center;
	width: 1.75rem; height: 1.75rem; border-radius: 50%;
	border: 1px solid var(--primary); background: var(--surface-2);
	color: var(--ink); font-size: .75rem; font-weight: 700;
}
.seriesrail {
	display: flex; gap: 2px; height: 4px; margin: .1rem .7rem 0 0;
}
.seriesrail span {
	flex: 1; min-width: 2px; border-radius: 2px; background: var(--surface-3);
}
.seriesrail span.started { background: var(--primary-dim); }
.seriesrail span.done { background: var(--primary); }


/* --------------------------------------------------------- series shelf */

/* The shelf is a numbered run, so it is an <ol> with its own markers
   suppressed: the position shown is the series' number, which is not
   always the row's ordinal — a novella at 1.5 is the second row and the
   list must not call it book two. */
.serieslist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.seriesvol {
	display: flex; gap: 1rem; align-items: flex-start;
	padding: .8rem 0; border-top: 1px solid var(--line);
}
.seriesvol:first-child { border-top: 0; }
.seriesvol .art {
	flex: 0 0 5rem; border-radius: var(--radius); overflow: hidden;
	border: 1px solid var(--line); display: block;
}
.seriesvol .art img { width: 100%; height: auto; display: block; }
.voltext { flex: 1; min-width: 10rem; display: flex; flex-direction: column; gap: .25rem; }
.voltext .title { font-weight: 600; }
.voltext .by { color: var(--muted); font-size: .9rem; }
.volnum {
	color: var(--muted); font-size: .8rem; font-variant-numeric: tabular-nums;
	letter-spacing: .04em;
}
.volactions { display: flex; gap: .5rem; align-items: center; }
.volactions .reading-status { margin: 0; }

@media (max-width: 46em) {
	.volactions { flex-basis: 100%; }
}

/* The assign dialog. Each row is a name and a position, and the position
   stays narrow so the name gets the width it needs. */
.assignrows { display: flex; flex-direction: column; gap: .4rem; margin: .8rem 0; }
.assignrow { display: flex; gap: .5rem; margin: 0; }
.assignrow input[type="text"] { min-width: 0; }
.assignrow input[name="name"] { flex: 1; }
.assignrow input[name="position"] { flex: 0 0 5rem; }

/* The names a merge or a split made lead to a shelf (ADR-0021). Each is
   a statement and the button that takes it back, so they sit on one
   line with the undo pushed to the end. */
.bindings { list-style: none; padding: 0; margin: .4rem 0 .8rem; }
.bindings li {
  display: flex; align-items: center; gap: .6rem;
  padding: .3rem 0; border-bottom: 1px solid var(--line);
}
.bindings li form { margin: 0 0 0 auto; }
