/* Shared theme tokens for every page (reader, library, login). The reader's
   pre-paint inline script sets body[data-theme] before first paint; these
   variables make the themes consistent across pages. */

/* Georgia Pro (licensed; Carter & Cone / Monotype), self-hosted from /fonts/.
   It buys ONE thing the bundled Georgia cannot: a Light weight. Light-on-dark
   text reads bolder than the same face on a light ground, and the bundled
   Georgia ships only 400/700 — browsers never synthesise a LIGHTER weight, so
   without this there is nothing to step down to. --reader-weight below spends
   it.
   Everything here is OPTIONAL. local() is tried first so a desktop install
   costs no download, the files are gitignored-by-absence rather than required,
   and the stack falls back to the system Georgia — so a checkout without
   public/fonts/ renders exactly as before, just without the Light step.
   Because the reader paginates from MEASURED metrics, a face that arrives
   after the first layout pass would rewrite every page break: reader-font.js
   settles document.fonts before the first pagination, behind the veil the
   reader already shows while a book loads. */
@font-face {
  font-family: "Georgia Pro";
  font-style: normal;
  font-weight: 300;
  font-display: block;
  src: local("Georgia Pro Light"), url("/fonts/GeorgiaPro-Light.woff2") format("woff2");
}
@font-face {
  font-family: "Georgia Pro";
  font-style: italic;
  font-weight: 300;
  font-display: block;
  src: local("Georgia Pro Light Italic"), url("/fonts/GeorgiaPro-LightItalic.woff2") format("woff2");
}
@font-face {
  font-family: "Georgia Pro";
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: local("Georgia Pro"), url("/fonts/GeorgiaPro-Regular.woff2") format("woff2");
}
@font-face {
  font-family: "Georgia Pro";
  font-style: italic;
  font-weight: 400;
  font-display: block;
  src: local("Georgia Pro Italic"), url("/fonts/GeorgiaPro-Italic.woff2") format("woff2");
}
@font-face {
  font-family: "Georgia Pro";
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: local("Georgia Pro Bold"), url("/fonts/GeorgiaPro-Bold.woff2") format("woff2");
}
@font-face {
  font-family: "Georgia Pro";
  font-style: italic;
  font-weight: 700;
  font-display: block;
  src: local("Georgia Pro Bold Italic"), url("/fonts/GeorgiaPro-BoldItalic.woff2") format("woff2");
}

:root {
  color-scheme: light;
  /* One paper for the whole app (CONSISTENCY_SYSTEM_PLAN.md move 1): the
     library page, the reader's reading surface, and the sidebar all sit on
     --bg. The light family is NEUTRAL (no green cast) so the single accent
     reads as the only colour voice; sepia/dark/night keep their own grounds. */
  --bg: #f7f7f4;
  --panel: #ffffff;
  --panel-strong: #fbfaf8;
  --text: #1d1c1a;
  --muted: #8a8984;
  --border: #e6e5e0;
  --accent: #1f6f5b;
  --accent-strong: #175545;
  /* THE destructive red (card menu delete + confirm buttons). One token, one
     meaning; never place it adjacent to routine actions (card-menu.md). */
  --danger: #b3402f;
  --shadow: 0 16px 40px rgba(37, 45, 35, 0.12);
  --control-bg: color-mix(in srgb, var(--panel), transparent 12%);
  --control-hover: color-mix(in srgb, var(--accent), transparent 91%);
  --control-ring: color-mix(in srgb, var(--accent), transparent 38%);
  /* Interaction-state tokens (touch-feedback pass): consolidate the press
     dim / motion timing / disabled opacity that were copy-pasted across the
     reader + library sheets. Press is a subtle scale-up + dim (flat, no fill). */
  --motion-fast: 120ms;
  --motion-press: 110ms;
  --state-press-opacity: 0.6;
  --state-press-scale: 1.08;
  --state-press-fill: var(--control-hover);
  --state-disabled-opacity: 0.45;
  --state-ring: var(--control-ring);
  /* ── Keycap chrome: THE ALIGNMENT CONTRACT ────────────────────────────────
     The corner Library key and the reading pane's shortcut keys derive size
     and top offset from these two tokens so they sit on one exact baseline —
     the design reads as a single key strip continuing across the panel
     divider only while the baselines match. Never position either side with
     independent hard-coded values (see reader-chrome design package,
     rationale.md "The alignment contract").
     20px (not the reference 16px) so the ? overlay's chips, which rise 19px
     above the keys, stay inside the viewport. */
  --key-size: 30px;
  --key-inset-top: 20px;
  --key-radius: 8px;
  /* Sidebar horizontal padding — also the corner key's left anchor. */
  --panel-pad-x: 20px;
  /* Visible glyph size for the mobile header icons (gear/back/audio); the 48px
     button is unchanged — this only grows the icon inside it. */
  --mobile-icon-size: 30px;
  --ui-font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --ui-size-xs: 11px;
  --ui-size-sm: 12px;
  --ui-size-md: 13px;
  --ui-size-page: 10px;
  --ui-size-title: 16px;
  --ui-size-brand: 18px;
  --ui-size-reader-heading: 22px;
  --ui-weight-normal: 400;
  --ui-weight-medium: 500;
  --ui-line-tight: 1.2;
  --ui-line-normal: 1.35;
  --reader-line: 1.58;
  --reader-font: "Georgia Pro", Georgia, "Times New Roman", serif;
  /* Body-copy weight, stepped down on the dark-ground themes to cancel the
     apparent bolding of light-on-dark text. Needs Georgia Pro (see @font-face
     above); with the bundled Georgia the browser just picks 400 and nothing
     changes. Headings and <strong> are deliberately NOT on this token. */
  --reader-weight: 400;
  /* Fine grain between Light and Regular. Georgia Pro is static — 300 then 400,
     nothing between — so an intermediate weight has to be synthesised by
     stroking the glyph outline in its own colour. Measured at 19px, the stroke
     closes the 300->400 gap by roughly: 0.1px 10%, 0.2px 22%, 0.3px 37%,
     0.4px 56%, 0.5px 80%. Two properties make this the right lever here:
     stroking does NOT change advance widths, so re-tuning it never moves a page
     break; and currentColor means the read-aloud playhead fill dims the stroke
     with the text for free. Keep it under ~0.4px — above that the antialiased
     stroke over the fill starts to read soft. */
  --reader-stroke: 0;
  --reader-size: 19px;
  --reader-block-space: clamp(24px, 3.2vh, 38px);
  --reader-controls-space: 48px;
  --reader-page-bottom-reserve: calc(var(--reader-size) * 3.16);
  --reader-inline-space: clamp(18px, 6vw, 96px);
  --mobile-actions-height: 0px;
  --app-height: 100vh;
  --mobile-bottom-reserve: 40px;
  --sidebar-width: 352px;
}

/* Keycap appearance, derived from the theme's own ink/ground so every theme
   (incl. Night's B=0 invariant) gets a native keycap for free. Declared on
   BODY, not :root: a custom property resolves its inner var() on the element
   it is declared on, and the theme's --text/--bg live on body[data-theme] —
   a :root copy would capture the light palette under every theme (the same
   trap the night --control-* redeclarations document below). */
body {
  --key-border: color-mix(in srgb, var(--text) 62%, var(--bg));
  --key-ink: var(--text);
  --key-hover-bg: color-mix(in srgb, var(--text) 8%, transparent);
}

body[data-theme="sepia"] {
  --bg: #f0eadc;
  --panel: #fbf6e9;
  --panel-strong: #f7f1e1;
  --text: #2c2418;
  --muted: #74674f;
  --border: #ddd0b8;
  --accent: #7d4b25;
  --accent-strong: #5e371a;
  --danger: #9c3a24;
}

body[data-theme="dark"] {
  color-scheme: dark;
  --reader-weight: 300;
  --reader-stroke: 0.3px;
  --bg: #151716;
  --panel: #1f2422;
  --panel-strong: #252c29;
  --text: #eff3ec;
  --muted: #a5b0a7;
  --border: #39423d;
  --accent: #65c5a7;
  --accent-strong: #8ad9c0;
  --danger: #e0705f;
  --shadow: 0 16px 40px rgba(0, 0, 0, 0.26);
}

/* Night shift — the zero-blue evening theme.
   EVERY colour here holds the blue channel at 0x00. On an OLED that switches
   the blue subpixel off outright, so the panel emits no blue light at all;
   test/theme-night.test.js asserts the invariant, so keep new values on it.
   Consequences worth knowing before editing (full analysis + measurements in
   plans/done/NIGHT_READING_THEME_PLAN.md):
   - B=0 confines the palette to the red-green edge of the gamut, so "brighter"
     and "yellower" are the same move. There is no hue axis left: hierarchy has
     to come from fill, weight and spacing, not colour.
   - --text is amber-gold, NOT red. Deep red caps at APCA Lc 37 (below every
     body-text floor) and sits +0.32 D from where the eye focuses under white
     light — outside the ~+/-0.2 D depth of focus of a dark-adapted pupil.
     #ffc400 reaches Lc 76, which clears the 19px serif floor.
   - --bg is a warm near-black rather than #000: the lift costs 0.4 Lc (nothing)
     and buys less halation and an escape from OLED near-black crush.
   - --accent is separated from --text by HUE (orange vs gold), not luminance,
     because --text already sits at the top of what this gamut can reach.
   The --control-* / --state-press-fill tokens are redeclared because the :root
   copies resolve their inner var(--accent) against :root's own (blue-bearing)
   accent, which would leak blue into hover and press states. */
body[data-theme="night"] {
  color-scheme: dark;
  /* color-scheme:dark makes the UA's initial text colour WHITE, and nothing
     else sets `color` on <body> — so any element that never gets an explicit
     colour inherits white and paints blue. Under the dark theme that default
     happens to look right and went unnoticed; here it breaks the invariant
     outright (it was reaching the masked mobile-header icons, which paint from
     currentColor). Pin the inherited colour to the theme's own ink. */
  color: var(--text);
  /* Night shift needs at least as much compensation as Dark: the ground/ink
     pair is higher contrast, and ~600 nm sits +0.22 D from focus (see the plan
     doc), which spreads stems optically on top of the irradiation effect.
     Georgia Pro is a STATIC family — Light is the floor, there is no 350, so
     --reader-stroke buys back part of the step to Regular. */
  --reader-weight: 300;
  --reader-stroke: 0.3px;
  --bg: #0d0600;
  --panel: #221a00;
  --panel-strong: #2d2300;
  --text: #ffc400;
  --muted: #d5a400;
  --border: #4c3a00;
  --accent: #ff8a00;
  --accent-strong: #ffd800;
  /* Danger stays on the B=0 invariant: a pure red-orange, separated from
     --accent by depth (no yellow), never the light themes' #b3402f (whose
     blue channel is 0x2f). */
  --danger: #ff3000;
  --shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
  --control-bg: color-mix(in srgb, var(--panel), transparent 12%);
  --control-hover: color-mix(in srgb, var(--accent), transparent 91%);
  --control-ring: color-mix(in srgb, var(--accent), transparent 38%);
  --state-press-fill: var(--control-hover);
  --state-ring: var(--control-ring);
  /* The remaining <img> chrome icons are solid-black glyphs whose antialiasing
     lives entirely in the ALPHA channel, so a colour-matrix chain repaints them
     without touching their edges — which is why the extreme contrast() below is
     free. The dark theme flips them to white with invert(1); white would put the
     blue subpixel back on and make the icons the brightest thing on screen, so
     night solves for black -> #ffc400 instead (exact; blue lands on 0). */
  --icon-tint: invert(19%) sepia(13%) saturate(1209%) hue-rotate(7deg)
    brightness(300%) contrast(299%);
  /* Book illustrations become an amber duotone. grayscale() runs first, so even
     a saturated blue photo enters as luminance and leaves with blue at 0 — the
     whole ramp stays on the invariant, topping out at #ffc300. */
  --image-tint: grayscale(1) brightness(0.66) sepia(1) saturate(4.4);
}

/* The <html> root paints the viewport canvas + iOS safe-area tint, and its
   color-scheme is what the browser paints the cross-document navigation
   backstop with. Custom properties only cascade downward, so the body-scoped
   theme blocks above never reach :root — the root would stay light and flash
   white when navigating library <-> reader on a dark theme. Mirror just --bg
   (and color-scheme for dark) here; the pre-paint inline scripts and the
   desktop theme switch set documentElement.dataset.theme so this resolves on
   the very first frame. The fuller token set stays on <body> (only descendants
   read --panel/--text/etc.).
   --reader-weight is mirrored here for the same reason as --bg: the reader's
   pre-paint script reads it off :root to decide WHICH body face to preload, so
   the theme -> weight mapping is not duplicated in inline script. */
:root[data-theme="sepia"] {
  --bg: #f0eadc;
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #151716;
  --reader-weight: 300;
}

:root[data-theme="night"] {
  color-scheme: dark;
  --bg: #0d0600;
  --reader-weight: 300;
}

/* Shared input baseline for every page (reader, library, login). Kills the
   Android tap-highlight grey flash globally and, on the chrome/nav controls
   only, closes the residual iOS-Safari tap delay (library/login load no other
   touch-action rule) while making nav bars + button labels non-selectable and
   callout-free. Reader body text (.bilingual-page) is deliberately excluded —
   selection there is intentional. The reader's more-specific touch-action:none
   on .reader/.page-progress-rail/.sheet-panel target non-buttons, so they win. */
* {
  -webkit-tap-highlight-color: transparent;
}

button,
[role="button"],
a.book-card,
.library-header,
.mobile-header,
.reader-controls,
.segmented,
.sheet-item {
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* Reduced motion keeps the press dim/colour but drops the lift: collapsing the
   scale token to 1 neutralises every `scale(var(--state-press-scale))` at once
   (and the page-turn slide is gated separately in styles.css). */
@media (prefers-reduced-motion: reduce) {
  :root {
    --state-press-scale: 1;
  }
}

/* Cross-document View Transitions (the library ⇄ reader crossfade) are DISABLED.
   On the iOS WKWebView the transition machinery captures snapshots and composites
   them, which produced: a blank frame → a flash of the OLD page redisplayed → the
   crossfade blurring both UIs together. A plain hard changeover (the default when
   nothing opts in) is cleaner there. The themed WKWebView background
   (ParalleloBridge) + the :root[data-theme] tokens keep the inter-document gap
   on-theme, so removing the crossfade does NOT bring back a white flash.
   Re-enable by setting navigation back to `auto`. */
@supports (view-transition-name: none) {
  @view-transition {
    navigation: none;
  }
}

/* Self-expiring floating notice (status.js creates it on demand on both
   pages). Bottom-centred, low-chrome, fades in/out; pointer-events:none so it
   never blocks a tap. The reader raises `bottom` above its transport bar in
   styles.css. */
.app-notice {
  position: fixed;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 24px);
  transform: translateX(-50%) translateY(8px);
  max-width: min(92vw, 30rem);
  padding: 9px 14px;
  border-radius: 12px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  font-family: var(--ui-font);
  font-size: var(--ui-size-md);
  line-height: 1.35;
  text-align: center;
  opacity: 0;
  transition: opacity 0.24s ease, transform 0.24s ease;
  pointer-events: none;
  z-index: 90;
}

.app-notice.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Upload progress pill (uploader.js creates it on both pages — like
   .app-notice above, shared components live here among the tokens). */
.upload-pill {
  position: fixed;
  right: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
  color: var(--muted);
  font-family: var(--ui-font);
  font-size: var(--ui-size-sm);
  line-height: var(--ui-line-tight);
  box-shadow: var(--shadow);
  pointer-events: none;
}
