:root {
  --bg: #0e1013;
  --bg-2: #15181d;
  --bg-3: #1c2027;
  --line: #272c34;
  --line-2: #333944;
  --ink: #e7eaef;
  --ink-2: #9aa3b0;
  --ink-3: #6b7480;
  --accent: #ffb340;
  --accent-ink: #1a1205;
  --ok: #4ec98a;
  --warn: #ffcf5c;
  --err: #ff7a72;
  --run: #5fb0ff;
  --radius: 10px;
  --sidebar: 380px;
  /* Strong curves. CSS's built-in easings lack the punch that makes motion feel
     deliberate rather than incidental. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
}

/* No height: 100% here. It caps body at exactly one viewport, and a sticky
   element can only stick inside its containing block - so the topbar came
   unstuck ~850px down a long book. body already gets min-height: 100vh below,
   which is what that rule was actually for. */

/* An author `display` rule beats the UA stylesheet's [hidden] { display: none },
   so any flex/grid container carrying the hidden attribute stays visible without
   this. Keep it a standalone selector - folding it into a selector list above
   once produced `html, [hidden] { display: none }`, which hides the document. */
[hidden] {
  display: none !important;
}

body {
  background: var(--bg);
  color: var(--ink);
  font: 14px/1.5 ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* ------------------------------------------------------------------ topbar */

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 18px;
  background: rgba(14, 16, 19, 0.92);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
}

.mark {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 4px;
  border: 2.5px solid var(--ink);
  background: #fff;
  box-shadow: inset 0 0 0 2px #fff, inset 0 0 0 3.5px #000;
}

.title-input {
  /* Runs out to the sidebar's right edge, so the title and the panels under it
     read as one column and the book caret lands on the divider rather than
     across it. The subtraction is what sits left of the field inside that width:
     the topbar's own padding, the mark, and the gap after it. Not flex: 1 - that
     stretched the field across the whole bar and dragged the caret with it. */
  flex: 0 1 auto;
  width: calc(var(--sidebar) - 18px - 20px - 10px);
  max-width: 46vw;
  min-width: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--ink);
  font-size: 16px;
  font-weight: 620;
  padding: 5px 8px;
}

.title-input:hover {
  border-color: var(--line);
}

.title-input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--bg-2);
}

.topbar-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

.badge {
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  background: var(--bg-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 10px;
  white-space: nowrap;
}

.badge.warn {
  color: var(--warn);
  border-color: #4a3c14;
  background: #241d09;
}

/* The header's one save control. A real button in both states: it used to be
   styled as bare text until something was unsaved, which - combined with an
   autosave that cleared the unsaved state after 700ms - meant it read as a
   label and never as something you could press. */
.save-state {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
  padding: 5px 11px;
  border: 1px solid var(--line-2);
  border-radius: 7px;
  background: var(--bg-3);
  color: var(--ink-2);
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

.save-state:hover { border-color: var(--line-2); background: var(--bg-2); }

/* Unsaved is the state worth shouting about, so it takes the accent and the
   solid fill - the same treatment as the primary action elsewhere in the bar. */
.save-state.dirty {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
}

.save-state.dirty:hover { filter: brightness(1.07); }
.save-state:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Saved is not a dead control - it is a true statement about the file, and it
   still reads as a button so the eye can find it before there is anything to
   save. Only the pointer changes. */
.save-state:disabled { cursor: default; opacity: 1; }

/* Amber while anything is unwritten, green once this book's file matches the
   screen. One dot, one meaning - it used to be hidden most of the time and to
   mean something else entirely (art missing from an exported file). */
.save-dot {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 3px rgba(78, 201, 138, 0.14);
}

/* On the accent fill the dot has to read against amber, so it goes solid dark
   rather than staying amber-on-amber. */
.save-state.dirty .save-dot {
  background: var(--accent-ink);
  box-shadow: 0 0 0 3px rgba(26, 18, 5, 0.18);
}

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

.shell {
  display: grid;
  grid-template-columns: var(--sidebar) minmax(0, 1fr);
  gap: 0;
  flex: 1;
  align-items: start;
}

.sidebar {
  position: sticky;
  top: 49px;
  max-height: calc(100vh - 49px);
  overflow-y: auto;
  padding: 14px;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 12px;
  scrollbar-width: thin;
}

.panel {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.panel-hero {
  border-color: var(--line-2);
  box-shadow: 0 0 0 1px rgba(255, 179, 64, 0.08), 0 8px 24px rgba(0, 0, 0, 0.3);
}

.panel-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.panel h2 {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.hint {
  font-size: 10.5px;
  color: var(--ink-3);
  margin-left: auto;
}

.micro {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--ink-3);
}

/* ------------------------------------------------------------- form fields */

textarea,
input[type="text"],
input[type="number"],
select {
  width: 100%;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line-2);
  border-radius: 7px;
  padding: 8px 9px;
  font: inherit;
  font-size: 13px;
}

textarea {
  resize: vertical;
  line-height: 1.5;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
}

.bible {
  min-height: 190px;
}

/* A field the current setting has taken out of play. Dimmed rather than hidden:
   it still holds words the user typed, and hiding those reads as losing them. */
.is-muted {
  opacity: 0.45;
}

textarea:focus,
input:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 179, 64, 0.13);
}

select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-2) 50%),
    linear-gradient(135deg, var(--ink-2) 50%, transparent 50%);
  background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 26px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  flex: 1;
}

.field > span {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.field-row {
  display: flex;
  gap: 8px;
}

.check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--ink);
  cursor: pointer;
}

.check input {
  accent-color: var(--accent);
  width: 15px;
  height: 15px;
}

input[type="range"] {
  accent-color: var(--accent);
  padding: 0;
  border: 0;
  background: transparent;
}

/* ----------------------------------------------------------------- buttons */

.btn {
  appearance: none;
  border: 1px solid var(--line-2);
  background: var(--bg-3);
  color: var(--ink);
  border-radius: 7px;
  padding: 7px 12px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 560;
  cursor: pointer;
  white-space: nowrap;
  transition: background 120ms ease, border-color 120ms ease, transform 160ms var(--ease-out);
}

/* Touch devices fire hover on tap, which leaves buttons stuck in a hover state
   after the finger lifts. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover:not(:disabled) {
    background: #242932;
    border-color: #414954;
  }
}

.btn:active:not(:disabled) {
  transform: scale(0.97);
}

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 680;
}

.btn-primary:hover:not(:disabled) {
  background: #ffc266;
  border-color: #ffc266;
}

.btn-quiet {
  background: transparent;
  color: var(--ink-2);
}

.btn-wide {
  width: 100%;
}

.btn-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.icon {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-3);
  border-radius: 6px;
  width: 26px;
  height: 26px;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  flex: none;
}

.icon:hover {
  background: var(--bg-3);
  border-color: var(--line-2);
  color: var(--ink);
}

.icon-danger:hover {
  color: var(--err);
  border-color: #5a2a27;
}

/* ------------------------------------------------------------------- queue */

.panel-queue {
  border-color: var(--line-2);
}

.queue-readout {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.chip {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  border-radius: 6px;
  padding: 3px 8px;
  background: var(--bg-3);
  border: 1px solid var(--line);
  color: var(--ink-2);
}

.chip b {
  color: var(--ink);
}

.bridge-note {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-2);
  background: #1b2230;
  border: 1px solid #2c3a52;
  border-radius: 8px;
  padding: 9px 10px;
}

.bridge-note b {
  display: block;
  color: var(--run);
  margin-bottom: 2px;
}

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  background: var(--bg);
  border: 1px solid var(--line-2);
  border-radius: 4px;
  padding: 1px 5px;
  color: var(--accent);
}

/* --------------------------------------------------------------- page grid */

.pages-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.pages {
  padding: 14px 14px 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-width: 0;
}

.page-card {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.page-card.is-cover {
  border-color: #4a3c14;
}

.page-card.is-back .page-label::after {
  content: ' · wraps with the front';
  color: var(--muted);
  font-weight: 400;
  letter-spacing: 0;
}

.page-card.is-running {
  border-color: #2c4a6d;
}

.page-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: var(--bg-3);
  border-bottom: 1px solid var(--line);
}

.page-label {
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  min-width: 66px;
}

.page-kind {
  width: auto;
  flex: none;
  font-size: 11.5px;
  padding: 4px 24px 4px 8px;
}

.spacer {
  flex: 1;
}

.status-pill {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: 999px;
  padding: 2.5px 9px;
  border: 1px solid var(--line-2);
  color: var(--ink-3);
}

.status-pill.queued {
  color: var(--accent);
  border-color: #4a3c14;
  background: #241d09;
}

.status-pill.running {
  color: var(--run);
  border-color: #2c4a6d;
  background: #101d2c;
}

.status-pill.done {
  color: var(--ok);
  border-color: #1f4a35;
  background: #0d1f16;
}

.status-pill.error {
  color: var(--err);
  border-color: #5a2a27;
  background: #241110;
}

.page-body {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  gap: 14px;
  padding: 12px;
}

/* ------------------------------------------------------------------- proof */

.proof {
  position: relative;
  aspect-ratio: 1 / 1;
  background: #fff;
  border-radius: 3px;
  box-shadow: 0 3px 14px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}

.proof-margin {
  position: absolute;
  inset: 4.47%; /* overwritten from print geometry */
  border: 1px dashed rgba(120, 140, 255, 0.55);
  pointer-events: none;
  z-index: 2;
}

/*
 * The barcode keep-out on a back cover. Size and offset come from
 * barcodeBox() - the numbers here are only a sane default for an unstyled load.
 * Deliberately loud: art drawn under it is not a small mistake, it is invisible
 * on the printed book, and it costs a regeneration to find out.
 */
.proof-barcode {
  position: absolute;
  right: 2.941%;
  bottom: 2.941%;
  width: 23.529%;
  height: 14.118%;
  /* Solid white, because the export paints a real white plate here and the proof
     has to show what the exported sheet will actually look like.
     The edge used to be a dashed amber line, which read as part of the ARTWORK -
     it was mistaken for a drawn box on the page, and the label for text on it.
     It is now a double keyline standing in for the decorative border the back
     cover ruleset asks the art to draw just outside this rectangle. */
  border: 1.5px solid rgba(255, 176, 60, 0.95);
  outline: 1px solid rgba(255, 176, 60, 0.45);
  outline-offset: 2px;
  background: #fff;
  display: grid;
  place-items: end center;
  pointer-events: none;
  z-index: 3;
}

.proof-barcode span {
  /* Sits UNDER the plate rather than inside it, so nothing on the white area can
     be read as something the page is carrying. */
  position: absolute;
  top: 100%;
  margin-top: 3px;
  font-size: 8.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(255, 176, 60, 0.9);
}

/*
 * The art box, inset by the print margin. This has to be a real wrapper: an
 * absolutely positioned <img> given both `inset` and `width/height:100%` is
 * over-constrained, and the browser drops `right`/`bottom` — so the image ends
 * up full-page-sized but offset, leaving margin on two sides only. Sizing the
 * wrapper by inset and letting the image fill it at 100% avoids that entirely,
 * and mirrors what renderPrintPage() does on the export canvas.
 */
.proof-art {
  position: absolute;
  inset: 4.47%; /* overwritten from print geometry */
  overflow: hidden;
  z-index: 1;
}

.proof-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.proof-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #b9bfc9;
  font-size: 11.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.proof.has-art .proof-empty {
  display: none;
}

.proof-meta {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

/* ------------------------------------------------------------ prompt column */

.prompt-col {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}

.prompt-col .scene {
  min-height: 128px;
}

.advanced summary {
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  cursor: pointer;
  padding: 3px 0;
}

.advanced summary:hover {
  color: var(--ink-2);
}

.advanced[open] {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
}

.page-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: auto;
}

.page-error {
  margin: 0;
  font-size: 11.5px;
  color: var(--err);
  background: #241110;
  border: 1px solid #5a2a27;
  border-radius: 7px;
  padding: 7px 9px;
  font-family: ui-monospace, Menlo, monospace;
  word-break: break-word;
}

/* ---------------------------------------------------------------- bottombar */


/* -------------------------------------------------------------- dialog/toast */

.dialog {
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--bg-2);
  color: var(--ink);
  max-width: min(760px, 92vw);
  width: 100%;
  padding: 0;
}

.dialog-narrow {
  max-width: min(440px, 92vw);
}

.unsaved-body {
  margin: 0;
  padding: 2px 14px 6px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-2);
}

.dialog::backdrop {
  background: rgba(0, 0, 0, 0.62);
}

.dialog header,
.dialog footer {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
}

.dialog header {
  border-bottom: 1px solid var(--line);
}

.dialog footer {
  border-top: 1px solid var(--line);
  justify-content: flex-end;
}

.dialog h3 {
  margin: 0;
  font-size: 13px;
  flex: 1;
}

.dialog pre {
  margin: 0;
  padding: 14px;
  max-height: 58vh;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, Menlo, monospace;
  font-size: 12px;
  line-height: 1.6;
  color: var(--ink-2);
}

.toast {
  position: fixed;
  left: 50%;
  bottom: 66px;
  transform: translateX(-50%);
  z-index: 60;
  background: var(--bg-3);
  border: 1px solid var(--line-2);
  border-radius: 8px;
  padding: 9px 14px;
  font-size: 12.5px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
  max-width: 78vw;
}

/* The action is inside the toast because the alternative - a dialog after a
   generation run - would interrupt exactly when the user is looking at new art. */
.toast-action {
  margin-left: 12px;
  padding: 3px 9px;
  border: 1px solid var(--line-2);
  border-radius: 6px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}

.toast-action:hover {
  border-color: var(--accent);
}

.toast.err {
  border-color: #5a2a27;
  color: var(--err);
}

/* ------------------------------------------------------------------ narrow */

@media (max-width: 1080px) {
  .shell {
    grid-template-columns: 1fr;
  }
  .sidebar {
    position: static;
    max-height: none;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  .page-body {
    grid-template-columns: 1fr;
  }
  .proof-col {
    max-width: 320px;
  }
}

/* ------------------------------------------------------------------ chrome */

.topbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Document menu. Native <details> so Escape, click-outside and keyboard focus
   all work without a line of JS. */
.menu {
  position: relative;
}

.menu-trigger {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.menu-trigger::-webkit-details-marker {
  display: none;
}

.menu-trigger::after {
  content: '';
  width: 0;
  height: 0;
  border: 3.5px solid transparent;
  border-top-color: currentColor;
  margin-top: 2px;
  transition: transform 160ms var(--ease-out);
}

.menu[open] .menu-trigger::after {
  transform: rotate(180deg);
}

.menu-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 40;
  min-width: 210px;
  padding: 5px;
  background: var(--bg-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  /* Scale from the trigger it belongs to, not from its own middle. A popover
     that grows from its centre reads as unrelated to the thing you clicked. */
  transform-origin: top right;
  animation: menu-in 150ms var(--ease-out);
}

@keyframes menu-in {
  from {
    /* Never scale(0) — nothing in the real world appears from nothing. */
    opacity: 0;
    transform: scale(0.97) translateY(-2px);
  }
}

.menu-pop button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  padding: 7px 9px;
  border-radius: 6px;
  cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
  .menu-pop button:hover {
    background: var(--bg-3);
  }
}

.menu-sep {
  height: 1px;
  background: var(--line);
  margin: 5px 4px;
}

/* -------------------------------------------------------------------- tabs */

.tabs {
  position: relative;
  display: flex;
  gap: 2px;
  padding: 3px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: 9px;
}

.tab {
  position: relative;
  z-index: 1;
  flex: 1;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: 12px;
  font-weight: 560;
  padding: 6px 4px;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 120ms ease;
}

.tab.is-active {
  color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .tab:not(.is-active):hover {
    color: var(--ink);
  }
}

/* Only the indicator moves. Switching tabs happens tens of times a session, so
   animating the panel content would tax the most repeated action in the app —
   but the indicator carries spatial continuity, which is worth 200ms. */
.tab-indicator {
  position: absolute;
  z-index: 0;
  top: 3px;
  bottom: 3px;
  background: var(--bg-3);
  border-radius: 6px;
  transition: transform 200ms var(--ease-out), width 200ms var(--ease-out);
}

.tab-count {
  display: inline-block;
  min-width: 15px;
  padding: 0 4px;
  border-radius: 20px;
  background: var(--bg-3);
  color: var(--ink-2);
  font-size: 10px;
  font-weight: 680;
  line-height: 15px;
  text-align: center;
}

.tab.is-active .tab-count {
  background: var(--accent);
  color: var(--accent-ink);
}

.tabpanels {
  display: flex;
  flex-direction: column;
}

.tabpanel {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* -------------------------------------------------------------------- cast */

.cast-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cast-card {
  background: var(--bg-3);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 9px;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.cast-head {
  display: flex;
  align-items: center;
  gap: 6px;
}

.cast-name {
  flex: 1;
  min-width: 0;
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--ink);
  font: inherit;
  font-size: 12.5px;
  font-weight: 680;
  letter-spacing: 0.02em;
  padding: 4px 6px;
}

.cast-name:focus {
  outline: none;
  background: var(--bg);
  border-color: var(--line-2);
}

.cast-field > span {
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-size: 10.5px;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.cast-field textarea {
  min-height: 44px;
}

.cast-tag {
  text-transform: none;
  letter-spacing: 0;
  font-size: 10px;
  color: var(--ink-3);
}

.cast-tag.is-cover {
  color: var(--accent);
}

.cast-empty {
  color: var(--ink-3);
  font-size: 12px;
  padding: 10px 0;
  text-align: center;
}

/* ----------------------------------------------------------------- palette */

.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.swatch {
  width: 34px;
  height: 34px;
  border-radius: 7px;
  border: 1px solid var(--line-2);
}

/* ---------------------------------------------------------------- page add */

/* Sits after the last card rather than pinned to the viewport: the moment you
   want another page is the moment you finish the previous one. */
.page-add {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  padding: 14px;
  margin-top: 2px;
}

/* --------------------------------------------------------- reduced motion */

/* Fewer and gentler, not none: opacity still communicates, movement is what
   causes trouble. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  .menu-pop {
    animation: none;
  }
}

/* ------------------------------------------------------------- extraction */

.extract-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  overflow: auto;
}

.extract-body h4 {
  margin: 12px 0 0;
  font-size: 11px;
  font-weight: 680;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
  display: flex;
  gap: 7px;
  align-items: baseline;
}

.extract-body h4:first-child {
  margin-top: 0;
}

.x-table {
  border-collapse: collapse;
  width: 100%;
  font-size: 12.5px;
}

.x-table td {
  padding: 5px 8px;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.x-name {
  width: 1%;
  white-space: nowrap;
  font-weight: 680;
  letter-spacing: 0.03em;
  color: var(--accent);
}

.x-quote {
  margin: 0;
  padding: 8px 10px;
  border-left: 2px solid var(--line-2);
  background: var(--bg);
  border-radius: 0 6px 6px 0;
  color: var(--ink-2);
  font-size: 12.5px;
}

.x-pre {
  margin: 0;
  padding: 10px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 7px;
  font-size: 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  color: var(--ink-2);
  max-height: 260px;
  overflow: auto;
}

/* ------------------------------------------------------------------ brief */

/* Wider than the other dialogs because it is two panes: what you pasted, and
   what it became. Seeing them side by side is the whole point - a preview you
   have to scroll away from the input to read is a preview nobody checks. */
.dialog-brief {
  max-width: min(1120px, 96vw);
}

.brief-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  min-height: 0;
}

.brief-input {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border-right: 1px solid var(--line);
  min-width: 0;
}

.brief-text {
  flex: 1;
  min-height: 46vh;
  resize: none;
  font-family: ui-monospace, Menlo, monospace;
  font-size: 12px;
  line-height: 1.6;
}

.brief-tools {
  display: flex;
  gap: 6px;
}

.brief-preview {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px;
  overflow: auto;
  max-height: 62vh;
  min-width: 0;
}

/* Same vocabulary as the extraction dialog: both answer "here is what I am
   about to do to your book", so they should not look like two features. */
.brief-preview h4 {
  margin: 12px 0 0;
  font-size: 11px;
  font-weight: 680;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
  display: flex;
  gap: 7px;
  align-items: baseline;
}

.brief-preview h4:first-child {
  margin-top: 0;
}

.brief-preview p {
  margin: 0;
}

.brief-preview .x-table td {
  font-size: 12px;
  line-height: 1.5;
}

.brief-preview .x-name {
  color: var(--ink-2);
}

/* A field this run will not touch, and the cast a page did not ask for. Dimmed
   rather than hidden: "left as it is" is information. */
.x-skip {
  color: var(--ink-3);
  font-style: italic;
}

.x-cast {
  display: block;
  margin-top: 3px;
  font-size: 11.5px;
  color: var(--accent);
}

.x-cast.x-skip {
  color: var(--ink-3);
}

.x-warn {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--warn);
}

.x-notes {
  margin: 0;
  padding-left: 16px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--ink-2);
}

.brief-mode {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  color: var(--ink-3);
}

.brief-mode select {
  width: auto;
}

.brief-warn {
  flex: 1;
  text-align: right;
}

.brief-warn.warn {
  color: var(--warn);
}

@media (max-width: 820px) {
  .brief-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .brief-input {
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }

  .brief-text {
    min-height: 26vh;
  }

  .brief-preview {
    max-height: 34vh;
  }
}

/* --------------------------------------------------------------- queue bar */

/* Pinned to the bottom of the page column. Sticky, not fixed: fixed would span
   the viewport and sit over the sidebar, which owns none of this. */
.queuebar {
  position: sticky;
  bottom: 0;
  z-index: 20;
  margin-top: auto;
  padding: 10px 14px;
  background: rgba(14, 16, 19, 0.94);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.queuebar-main {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.queuebar .queue-readout {
  flex: 1;
  min-width: 0;
}

.queuebar .btn-row {
  flex: none;
}

/* The page column has to be able to grow, or margin-top:auto has nothing to
   push against and the bar floats mid-screen on a short book. */
.pages-col {
  min-height: calc(100vh - 49px);
}

/* ------------------------------------------------------------ sheet pages */

/* A sheet is reference art, not a page of the book — worth reading as different
   at a glance, since it is excluded from every export. */
.page-card.is-sheet {
  border-color: #3a4a63;
}

.page-card.is-sheet .page-label::after {
  content: 'reference';
  margin-left: 8px;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--run);
  background: #16202c;
  border: 1px solid #24384f;
}

.cast-views {
  flex: none;
  font-size: 11px;
  padding: 4px 8px;
}

/* One per row: the sidebar is 380px, so a card gets full width and its 2x2
   views stay big enough to actually judge. */
.cast-cards {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.char-card {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.char-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 8px 8px 10px;
  border-bottom: 1px solid var(--line);
}

.char-name {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.03em;
  padding: 3px 6px;
}

.char-name:focus {
  outline: none;
  background: var(--bg);
  border-color: var(--line-2);
}

.char-tabs {
  display: flex;
  gap: 1px;
  padding: 2px;
  background: var(--bg);
  border-radius: 7px;
}

.char-tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font: inherit;
  font-size: 10.5px;
  font-weight: 600;
  padding: 3px 7px;
  border-radius: 5px;
  cursor: pointer;
  transition: color 120ms ease, background 120ms ease;
}

.char-tab.is-on {
  background: var(--bg-3);
  color: var(--ink);
}

/* Only the chosen panel renders. Card tabs get flipped constantly while
   comparing words against art, so there is no transition on the swap. */
.char-panel {
  display: none;
  padding: 10px;
  flex-direction: column;
  gap: 8px;
}

.char-body[data-tab="views"] [data-cp="views"],
.char-body[data-tab="details"] [data-cp="details"],
.char-body[data-tab="prompt"] [data-cp="prompt"] {
  display: flex;
}

.char-proof {
  aspect-ratio: 1;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 7px;
  overflow: hidden;
}

/* The sheet is a 2x2 grid, so zooming one view is a background-position move -
   no cropping, no second request, no extra image in memory. */
.char-shot {
  width: 100%;
  height: 100%;
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  transition: background-size 220ms var(--ease-out), background-position 220ms var(--ease-out);
}

.char-proof[data-zoom="0"] .char-shot { background-size: 200% 200%; background-position: 0% 0%; }
.char-proof[data-zoom="1"] .char-shot { background-size: 200% 200%; background-position: 100% 0%; }
.char-proof[data-zoom="2"] .char-shot { background-size: 200% 200%; background-position: 0% 100%; }
.char-proof[data-zoom="3"] .char-shot { background-size: 200% 200%; background-position: 100% 100%; }

.char-proof.is-empty {
  display: grid;
  place-items: center;
  background: var(--bg);
  border-style: dashed;
  border-color: var(--line-2);
  color: var(--ink-3);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.char-views {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.char-view-btn {
  appearance: none;
  border: 1px solid var(--line);
  background: var(--bg-3);
  color: var(--ink-3);
  font: inherit;
  font-size: 10.5px;
  padding: 3px 7px;
  border-radius: 999px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}

.char-view-btn.is-on {
  color: var(--accent);
  border-color: #4a3c14;
}

.char-prompt {
  margin: 0;
  padding: 9px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 7px;
  font-size: 11px;
  line-height: 1.5;
  white-space: pre-wrap;
  color: var(--ink-2);
  max-height: 320px;
  overflow: auto;
}


/* The cost of what is queued right now, as opposed to the whole-book estimate
   in the top bar. Different number, different question. */
.chip-cost {
  color: var(--accent);
  border-color: #4a3c14;
  background: #241d09;
}

/* --------------------------------------------------------------- page cast */

.page-cast-field {
  gap: 5px;
}

.page-cast {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.cast-chip {
  appearance: none;
  border: 1px solid var(--line-2);
  background: var(--bg-3);
  color: var(--ink-3);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 4px 9px;
  border-radius: 999px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease,
    transform 160ms var(--ease-out);
}

.cast-chip:active {
  transform: scale(0.96);
}

.cast-chip.is-on {
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--accent);
}

/* Cast, but with no sheet behind them: they contribute words to the prompt and
   no reference image. */
.cast-chip.is-on.no-sheet {
  background: #3a2f12;
  border-color: #4a3c14;
  color: var(--warn);
}

.cast-refs {
  margin-left: auto;
  text-transform: none;
  letter-spacing: 0;
  font-size: 10px;
  color: var(--ink-3);
}

.cast-refs.warn {
  color: var(--warn);
}

.char-card .char-tabs {
  flex: none;
}

/* --------------------------------------------------------------- scene lint */

.scene-lint {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.lint {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 9px;
  border-radius: 7px;
  border: 1px solid #4a3c14;
  background: #211a08;
  font-size: 11.5px;
  line-height: 1.4;
}

.lint b {
  color: var(--warn);
  font-weight: 640;
}

.lint span {
  color: var(--ink-3);
}

/* A colour word on an interior is the oldest bug in this project, so it reads
   louder than a staging suggestion. */
.lint-colour {
  border-color: #5a2320;
  background: #240f0e;
}

.lint-colour b {
  color: var(--err);
}

/* ------------------------------------------------------------- size lineup */

/* The chart is drawn from the numbers, not from the art, so it is readable
   before a single credit is spent - which matters, because the drift it exists
   to catch is invisible on any one page and only shows up side by side. */
.lineup-chart {
  display: grid;
  gap: 6px;
  margin-bottom: 12px;
}

.lineup-row {
  display: grid;
  grid-template-columns: minmax(56px, 22%) 1fr 40px;
  align-items: center;
  gap: 8px;
}

.lineup-name {
  font-size: 12px;
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Bars share one baseline and one scale: the tallest fills the track, so the
   ratio between two characters is the ratio between two bar lengths. */
.lineup-bar {
  background: var(--surface-3, rgba(127, 127, 127, 0.16));
  border-radius: 999px;
  height: 10px;
  overflow: hidden;
}

.lineup-bar > i {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 180ms var(--ease-out);
}

.lineup-pct {
  font-size: 11px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.lineup-proof {
  aspect-ratio: 1 / 1;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius-2, 10px);
  overflow: hidden;
  margin: 12px 0;
  display: grid;
  place-items: center;
}

.lineup-shot {
  width: 100%;
  height: 100%;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}

.lineup-empty {
  color: var(--ink-3);
  font-size: 12px;
  text-align: center;
}

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

/* Sits left of the title and scopes it: everything to the right belongs to the
   selected book. Sized to the longest title rather than fixed, so a library of
   one short-named book does not reserve half the bar. */
/* Width is the point: the control is clipped to its caret so the selected
   option's text never renders beside the title input, which already says it.
   The popup the caret opens is drawn by the platform and is unaffected. */
.book-select {
  width: 26px;
  flex: none;
  padding: 0;
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid transparent;
  border-radius: var(--radius-1, 8px);
  background: transparent;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-2) 50%),
    linear-gradient(135deg, var(--ink-2) 50%, transparent 50%);
  background-size: 5px 5px, 5px 5px;
  background-position: calc(50% - 3px) 55%, calc(50% + 1px) 55%;
  background-repeat: no-repeat;
  color: transparent;
  font: inherit;
  height: 26px;
  cursor: pointer;
  text-indent: -999px;
}

.book-select:hover { border-color: var(--line); background-color: var(--surface-2, rgba(127, 127, 127, 0.1)); }
.book-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The popup the caret opens. Anchored to the caret rather than the header so it
   lines up with the control the user actually clicked. */
.book-menu-wrap { position: relative; flex: none; display: flex; }

.book-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 60;
  min-width: 260px;
  max-width: 380px;
  max-height: 60vh;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius-1, 8px);
  background: var(--bg-3);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
}

.book-menu[hidden] { display: none; }

/* One row per book. The row is the switch target; the delete button sits inside
   it but is a separate button, so a click on the icon never also switches. */
.book-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 8px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.book-row:hover,
.book-row:focus-visible { background: var(--line); outline: none; }
.book-row:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }

.book-row-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.book-row-pages { flex: none; color: var(--ink-3); font-size: 12px; }
.book-row.is-active .book-row-title { font-weight: 600; }

/* Hidden until the row is hovered or something inside it has keyboard focus, so
   the destructive action is never the thing your eye lands on first. Keyboard
   users reach it by tabbing, which is why :focus-within counts. */
.book-del {
  flex: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
  opacity: 0;
}

.book-row:hover .book-del,
.book-row:focus-within .book-del,
.book-del:focus-visible { opacity: 1; }

.book-del:hover { background: rgba(255, 122, 114, 0.16); color: var(--err); }
.book-del:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.book-del svg { width: 14px; height: 14px; display: block; }

/* Touch has no hover. Showing the icon always is better than an action that
   cannot be reached at all. */
@media (hover: none) {
  .book-del { opacity: 1; }
}

.book-menu-sep { height: 1px; margin: 4px 6px; background: var(--line); }

/* Reordering. The grip is the only always-visible hint that a row moves; it
   brightens with the row so it does not compete with the title for attention. */
.book-grip {
  flex: none;
  display: grid;
  place-items: center;
  width: 14px;
  height: 18px;
  margin-left: -2px;
  color: var(--ink-3);
  opacity: 0.55;
  cursor: grab;
}

.book-row:hover .book-grip { opacity: 1; }
.book-grip svg { width: 12px; height: 12px; display: block; }
.book-row.is-dragging { opacity: 0.4; cursor: grabbing; }

/* The drop line. Drawn on the row rather than as a floating element so it can
   never be left behind on the page after the drag ends - clearing the class
   clears the line. Inset to the row's own padding so it reads as a gap between
   two rows and not as a border on one of them. */
.book-row.drop-above::before,
.book-row.drop-below::after {
  content: "";
  position: absolute;
  left: 6px;
  right: 6px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  pointer-events: none;
}

.book-row.drop-above::before { top: -2px; }
.book-row.drop-below::after { bottom: -2px; }
