/*
  App stylesheet — shares the marketing site's palette, type, and component
  language (taop.xyz/themes/taop/static/css/style.css) so the app reads as
  part of the same product, not a bolted-on tool. Palette values below are
  taken directly from that stylesheet, including the less common tokens used
  in its own "course" landing page (#f8f7f9 panel tint, #dfdde1/#b8a9c9
  muted-on-dark text, the #372649→#4a3666 gradient) — this app's CTA boxes
  reuse that exact gradient-box pattern rather than inventing a new one.
*/

@font-face {
  font-family: 'Bookman';
  src: url('/fonts/BookmanStdLight.woff2') format('woff2');
  font-weight: 100;
  font-style: normal;
}
@font-face {
  font-family: 'Bookman';
  src: url('/fonts/BookmanStdLightItalic.woff2') format('woff2');
  font-weight: 100;
  font-style: italic;
}
@font-face {
  font-family: 'Bookman';
  src: url('/fonts/BookmanStdMedium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
}
@font-face {
  font-family: 'Bookman';
  src: url('/fonts/BookmanStdMediumItalic.woff2') format('woff2');
  font-weight: 500;
  font-style: italic;
}

:root {
  --ink:            #2e2241;
  --heading:        #372649;
  --body-text:      #67527a;
  --muted:          #896da1;
  --accent:         #60b2d3;
  --bg:             #f9f9f9;
  --panel-bg:       #ffffff;
  --panel-alt:      #f8f7f9;
  --border:         #e5e3e7;
  --header-bg:      #372649;
  --footer-bg:      #181224;
  --code-bg:        #272822;
  --gradient-end:   #4a3666;
  --muted-on-dark:  #dfdde1;
  --faint-on-dark:  #b8a9c9;
  --danger:         #ff6b6b; /* the real site's own error/urgent red (.early_note, .banner_badge) — was an invented #a4454f */
  --success:        #3e8e41;
}

* { box-sizing: border-box; }
html, body { min-height: 100%; }
body {
  margin: 0;
  font: 16px/1.6 'Lato', sans-serif;
  color: var(--body-text);
  background: var(--bg);
  padding-top: 71px;
}
h1, h2, h3, h4, h5, h6 { margin: 0 0 12px 0; color: var(--heading); font-family: 'Bookman', Georgia, serif; }
h1 { font: 400 34px 'Bookman'; }
h2 { font: 300 26px 'Bookman'; }
h3 { font: 500 22px 'Bookman'; }
p { margin: 0 0 16px 0; }
a { color: var(--ink); text-decoration: none; transition: color .2s linear; }
a:hover { color: var(--accent); }
code, pre { font-family: 'JetBrains Mono', 'Fira Code', monospace; }
pre {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 14px 16px;
  overflow-x: auto;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
}
:not(pre) > code {
  background: var(--panel-alt);
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 0.85em;
}

.wrapper {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 20px;
}

/* Dev-only toggle bar (internal/devtools) — content layer + access
   simulation. Only ever rendered when config.AuthDevBypass is set, same
   gate as /dev/login; visually loud on purpose (it's a dev tool, not
   product chrome) so it's never mistaken for something a real visitor
   would see. */
.dev-toggle-bar {
  background: #1a1a1a;
  color: #fff;
  font: 600 12px 'Lato', sans-serif;
  padding: 6px 20px;
  display: flex;
  align-items: center;
  gap: 20px;
  position: relative;
  z-index: 1001;
}
/* .header is position:fixed at top:0, so a normal-flow dev bar before
   it would render underneath it, not above — push the fixed header (and
   the body padding sized for it) down by the bar's own height instead. */
body.has-dev-bar .dev-toggle-bar { position: fixed; top: 0; left: 0; right: 0; }
body.has-dev-bar .header { top: 32px; }
body.has-dev-bar { padding-top: 103px; }
.dev-toggle-label { background: #e5a95e; color: #1a1a1a; padding: 2px 8px; border-radius: 3px; font-weight: 700; }
.dev-toggle-group { display: flex; align-items: center; gap: 8px; color: #aaa; }
.dev-toggle-group a { color: #fff; text-decoration: underline; }
.dev-toggle-group a.active { color: #60b2d3; text-decoration: none; font-weight: 700; }

/* Header — exact structural clone of the marketing site's own homepage nav
   (taop.xyz/themes/taop/layouts/partials/header.html + the relevant rules
   from its style.css: ".header", ".header nav ul li", etc., ported here
   under the same class names on purpose, for 1:1 comparison). Used only
   on this app's home page ("/") — see partials.html's "header" template
   doc comment for what's deliberately identical vs. adapted. */
.header {
  position: fixed;
  top: 0; left: 0; width: 100%;
  z-index: 999;
  background: var(--header-bg);
}
/* Same background as the real site's header on the contents page
   (body.contents-page .header, style.css): the slide image, not a flat
   fill — but only where a hero with that same image sits right below it.
   :has(+ .home-hero) targets exactly that, with no per-page body-class
   needed: same image, position, and fixed attachment as .home-hero, so
   the two form one continuous backdrop instead of a visible seam. */
.header:has(+ .home-hero) {
  background: var(--header-bg) url('/images/hero-bg.jpg') center top no-repeat;
  background-size: cover;
  background-attachment: fixed;
}
.header .wrapper {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 70px;
}
.header nav ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  list-style: none;
  margin: 0; padding: 0;
}
.header nav ul li {
  font: 300 13px 'Lato';
  text-transform: uppercase;
  padding: 25px 15px;
}
.header nav ul li.divider {
  width: 1px;
  height: 20px;
  background: rgba(255,255,255,0.3);
  padding: 0;
  margin: 0 5px;
}
.header nav ul li a.logo {
  font-size: 0;
  line-height: 0;
  width: 40px;
  height: 40px;
  display: block;
  background: url('/images/logo.png') no-repeat center center;
  background-size: 40px auto;
}
.header nav ul li a,
.header nav ul li a:visited {
  color: #fff;
  transition: font linear 0.2s;
  letter-spacing: 2px;
  text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
/* Icons ride along in the markup (for the app-shell sidebar's use of the
   same Font Awesome glyphs elsewhere) but stay hidden here, same as the
   real site — this nav is text-only. */
.header nav ul li a i { display: none; }
.header nav ul li:first-child,
.header nav ul li a:hover { font-weight: 700; }
.header .sticky-cta-wrapper {
  position: absolute;
  top: 50%; right: 20px;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 18px;
}
.header .sticky-cta-wrapper .button.button_purple {
  background: var(--accent);
  border-color: var(--heading);
}
.header .sticky-cta-wrapper .button.button_purple:hover {
  background: var(--heading);
  border-color: var(--accent);
}
.header .sticky-cta {
  font-size: 11px;
  padding: 8px 20px;
  min-height: auto;
}

/* App-shell nav — the logged-in, paid-content area (/book, /courses,
   /lab — NOT the dashboard/home page, which uses the marketing nav since
   it's a valid entry point for first-time visitors too). A vertical left
   sidebar, per the cited SaaS convention (Pencil & Paper: nav "shifts from
   a horizontal layout at the top to a tall and narrow vertical bar aligned
   with the page's left edge" once inside the product) rather than a
   variant of the marketing header. body.app-shell-page zeroes out the
   global body { padding-top: 71px } (sized for .header being
   position:fixed) — the sidebar layout needs none of it, since it isn't a
   fixed-position top bar at all. */
body.app-shell-page { padding-top: 0; }
.app-shell-layout { display: flex; align-items: stretch; min-height: 100vh; }
/* Same background treatment as .home-hero/.header:has(+.home-hero)
   — the slide image + dark-ink overlay, fixed attachment — so the sidebar
   reads as the same brand surface as the hero, not a generic light SaaS
   panel. Text colors below are the hero's own light-on-dark palette
   (--muted-on-dark, white) rather than the light-sidebar values used
   before. */
.app-shell-sidebar {
  flex: 0 0 240px;
  display: flex;
  flex-direction: column;
  background: var(--header-bg) url('/images/hero-bg.jpg') center top no-repeat;
  background-size: cover;
  background-attachment: fixed;
  padding: 24px 20px;
  /* Sticky, not scroll-with-content: on a long chapter the sidebar nav
     (and the sign-out it holds) should stay put rather
     than scrolling out of view with the prose. align-self: flex-start
     is needed alongside sticky since the parent (.app-shell-layout) is
     a stretch-aligned flex row, which would otherwise force this
     column to the height of the (taller) main content and defeat
     sticky positioning entirely. own overflow-y so a viewport shorter
     than the nav's own content can still scroll just the sidebar. */
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100vh;
  overflow-y: auto;
}
.app-shell-logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  color: #fff;
  font: 700 15px 'Lato';
  font-variant: small-caps;
  letter-spacing: 0.5px;
  margin-bottom: 20px;
}
.app-shell-logo:hover { color: #fff; }
.app-shell-logo img { width: 36px; height: 36px; }
.app-shell-divider { border: none; border-top: 1px solid rgba(255,255,255,0.15); margin: 0 0 20px 0; }
.app-shell-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.app-shell-nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--muted-on-dark);
  font: 500 14px 'Lato';
  padding: 10px 12px;
  border-radius: 6px;
  border-left: 3px solid transparent;
}
.app-shell-nav a i { width: 16px; text-align: center; color: var(--accent); }
.app-shell-nav a:hover { color: #fff; background: rgba(255,255,255,0.08); }
.app-shell-nav a.active { color: #fff; background: rgba(255,255,255,0.12); border-left-color: var(--accent); font-weight: 700; }
.app-shell-nav-group { margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.15); }
.app-shell-nav-group-label {
  font: 700 10px 'Lato'; letter-spacing: 1px; text-transform: uppercase;
  color: var(--faint-on-dark); padding: 0 12px 6px 12px;
}
/* Sidebar "Add Bookmark" — same visual shape as .app-shell-nav a (icon +
   label, hover state), but a <form>/<button> (a POST action) and a plain
   <span> (already-bookmarked state) instead of a link. */
.app-shell-nav-bookmark-form { margin: 0; }
.app-shell-nav-bookmark-form button,
.app-shell-nav-bookmarked {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  color: var(--muted-on-dark);
  font: 500 14px 'Lato';
  padding: 10px 12px;
  border-radius: 6px;
  border: none;
  border-left: 3px solid transparent;
  background: transparent;
  text-align: left;
  cursor: pointer;
}
.app-shell-nav-bookmark-form button i,
.app-shell-nav-bookmarked i { width: 16px; text-align: center; color: var(--accent); }
.app-shell-nav-bookmark-form button:hover { color: #fff; background: rgba(255,255,255,0.08); }
.app-shell-nav-bookmarked { color: var(--accent); cursor: default; }
.app-shell-account { display: flex; flex-direction: column; gap: 8px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.15); }
.app-shell-account a { color: var(--muted-on-dark); font: 500 13px 'Lato'; padding: 4px 12px; }
.app-shell-account a:hover { color: #fff; }
.app-shell-main { flex: 1; min-width: 0; background: var(--bg); }
@media (max-width: 720px) {
  .app-shell-layout { flex-direction: column; }
  .app-shell-sidebar {
    flex: 0 0 auto;
    flex-direction: row;
    align-items: center;
    padding: 12px 16px;
    gap: 16px;
    background-attachment: scroll;
    height: auto;
    overflow: visible;
  }
  .app-shell-logo { flex-direction: row; margin-bottom: 0; }
  .app-shell-logo span { display: none; }
  .app-shell-divider { display: none; }
  .app-shell-nav { flex-direction: row; gap: 2px; }
  .app-shell-nav a { padding: 8px; }
  .app-shell-nav a span { display: none; }
  .app-shell-nav-bookmark-form button, .app-shell-nav-bookmarked { padding: 8px; width: auto; }
  .app-shell-nav-bookmark-form button span, .app-shell-nav-bookmarked span { display: none; }
  .app-shell-nav-group { margin-top: 0; padding-top: 0; border-top: none; display: flex; gap: 2px; }
  .app-shell-nav-group-label { display: none; }
  .app-shell-account { flex-direction: row; border-top: none; padding-top: 0; }
}

/* Buttons — same shape/language as the marketing site */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 26px;
  min-height: 40px;
  font: 500 13px 'Lato';
  letter-spacing: 1.5px;
  text-transform: uppercase;
  border-radius: 20px;
  border: 1px solid var(--heading);
  color: var(--heading);
  background: transparent;
  cursor: pointer;
  transition: all .2s linear;
}
.button:hover { background: var(--heading); color: #fff; }
.button i { margin-right: 8px; font-size: 13px; }
.button.button_purple { background: var(--heading); color: #fff; border-color: var(--heading); }
.button.button_purple:hover { background: var(--accent); border-color: var(--accent); }
.button.button_large { font-size: 15px; min-height: 50px; padding: 12px 36px; }
.button[disabled] { opacity: .5; cursor: not-allowed; }

/* Layout containers */
.container { padding: 110px 0 60px 0; min-height: calc(100vh - 260px); }
.container.narrow .wrapper { max-width: 560px; }

.card {
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 28px;
  margin-bottom: 24px;
  box-shadow: 0 2px 6px rgba(0,0,0,0.04);
}

/* sent.html's dev-only magic-link poll (see AuthHandlers.DevMagicLinkPoll)
   — same amber "this is a dev tool" accent as .dev-toggle-label. */
.dev-magic-link {
  margin-top: 8px;
  padding: 12px 16px;
  border: 1px dashed #e5a95e;
  border-radius: 6px;
  background: rgba(229,169,94,0.08);
  font-size: 14px;
}
.dev-magic-link p { margin: 0; }
.dev-magic-link-found a { word-break: break-all; }

/* sent.html's illustrated 3-step flow — mirrors taop.xyz's own
   post-purchase /welcome/ page (layouts/page/welcome.html), reusing this
   app's --heading/--body-text/--muted variables in place of that page's
   hardcoded hex. #7c55b4/#e4dced (active step + connector line) aren't
   already app.css variables — one-off, scoped to this component only. */
.welcome-page {
  padding: 40px 20px 20px;
  text-align: center;
}
.welcome-page .welcome-icon {
  font-size: 2.6rem;
  color: var(--heading);
  margin-bottom: 16px;
  display: block;
}
.welcome-lede {
  font-size: 1.15rem;
  color: var(--body-text);
  margin: 0 auto 32px;
  max-width: 480px;
  line-height: 1.6;
}
.welcome-steps {
  display: flex;
  justify-content: center;
  gap: 0;
  max-width: 680px;
  margin: 32px auto 40px;
  position: relative;
}
.welcome-steps::before {
  content: '';
  position: absolute;
  top: 28px;
  left: calc(16.66% + 28px);
  right: calc(16.66% + 28px);
  height: 2px;
  background: #e4dced;
  z-index: 0;
}
.welcome-step {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  position: relative;
  z-index: 1;
  padding: 0 12px;
}
.welcome-step-num {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--heading);
  color: #fff;
  font: 700 20px 'Lato', sans-serif;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.welcome-step-num.active {
  background: #7c55b4;
  box-shadow: 0 0 0 4px #e4dced;
}
.welcome-step-label {
  font-size: .88rem;
  color: var(--body-text);
  line-height: 1.4;
  max-width: 140px;
}
.welcome-step-label strong {
  display: block;
  color: var(--heading);
  font-size: .95rem;
  margin-bottom: 3px;
}
.welcome-help {
  font-size: .88rem;
  color: var(--muted);
  line-height: 1.7;
}
.welcome-help a {
  color: var(--heading);
  font-weight: 700;
}
.welcome-help a:hover {
  color: var(--accent);
}
@media (max-width: 720px) {
  .welcome-steps {
    flex-direction: column;
    align-items: center;
    gap: 24px;
  }
  .welcome-steps::before { display: none; }
  .welcome-step { flex-direction: row; text-align: left; padding: 0; }
  .welcome-step-label { max-width: none; }
}

/* Forms */
label { display: block; font: 700 11px 'Lato'; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
input[type=text], input[type=email], textarea {
  width: 100%;
  font: 15px 'Lato';
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 10px 14px;
  height: 44px;
  margin-bottom: 16px;
}
textarea { height: 120px; font-family: 'JetBrains Mono', monospace; font-size: 13px; }
.flash { padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; font-size: 14px; }
.flash.info { background: #eef7fa; color: var(--ink); border: 1px solid var(--accent); }
/* Not var(--danger) here on purpose: the real site's #ff6b6b coral is
   designed as an accent on dark backgrounds (badges, dark CTA boxes) — as
   body text on this light pink it fails contrast (~2.6:1, WCAG AA needs
   4.5:1). This is a plain readable dark red for a case the real site
   doesn't have an equivalent of, not a brand-palette extraction. */
.flash.error { background: #fbeaea; color: #a4454f; border: 1px solid var(--danger); }

.dashboard-product-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.dashboard-section-title { margin: 40px 0 16px 0; }
.dashboard-vip-note {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 18px;
  margin-top: 14px;
  font-size: 14px;
}

/* Dashboard: registration summary ("what you have") — plain bordered rows,
   a dense list of facts to scan (courses_home's per-course tier list). */
.registration-summary {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 8px;
}
.registration-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 18px;
  background: var(--panel-bg);
}
.registration-row + .registration-row {
  border-top: 1px solid var(--border);
}
.registration-product { font-weight: 700; color: var(--heading); }
.registration-tier {
  font: 700 11px 'Lato';
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--panel-alt);
  padding: 4px 10px;
  border-radius: 10px;
}
.registration-tier-full { color: var(--success); }
/* Same badge, dropped onto a dark hero (courses_home's home-hero-cta) —
   the light --panel-bg/--muted pairing above reads as invisible on the
   hero's dark-ink background, so this swaps in the light-on-dark palette
   already established for .home-hero-subtitle/.home-hero-stat-l. */
.home-hero .registration-tier {
  background: rgba(255,255,255,0.12);
  color: #fff;
}

/* Dashboard: two-column registration+progress+action panel, one column
   per product — replaces what used to be three separate sections
   (registrations list, progress list, "more from TAOP" upsell cards) so
   the "buy what's missing"/"upgrade"/"order the print edition" actions
   sit right next to the registration/progress facts they act on. */
.dashboard-products {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}
@media (max-width: 720px) {
  .dashboard-products { grid-template-columns: 1fr; }
}
.dashboard-product-panel {
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 22px;
}
.dashboard-product-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 14px;
}
.dashboard-product-name {
  font: 700 18px 'Bookman', serif;
  color: var(--heading);
}
.dashboard-product-name:hover { color: var(--accent); }
.dashboard-product-panel .progress-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin-bottom: 16px;
}
.progress-label { font-weight: 700; color: var(--heading); }
.progress-count { font-size: 14px; color: var(--muted); }
.dashboard-note-muted { font-size: 14px; color: var(--muted); margin-top: 10px; }
.dashboard-note-quizzes { margin-top: 24px; }

.whats-new-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }
.whats-new-item {
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  padding: 12px 16px;
}
.whats-new-kind {
  font: 700 11px 'Lato';
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--accent);
}
.whats-new-title { margin: 4px 0 4px 0; font: 700 16px 'Bookman', serif; color: var(--heading); }
.whats-new-summary { margin: 0; font-size: 14px; color: var(--muted); }

.dashboard-bookmarks { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
@media (max-width: 800px) { .dashboard-bookmarks { grid-template-columns: 1fr; } }
.dashboard-bookmark-group-title { margin: 0 0 10px 0; font: 700 13px 'Lato'; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); }
.bookmark-list { list-style: none; margin: 0; padding: 0; }
.bookmark-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.bookmark-link { display: flex; flex-direction: column; gap: 2px; color: var(--body-text); text-decoration: none; }
.bookmark-link:hover .bookmark-title { text-decoration: underline; }
.bookmark-label { font: 700 11px 'Lato'; letter-spacing: 1px; text-transform: uppercase; color: var(--accent); }
.bookmark-title { font-size: 15px; }
.bookmark-remove {
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
}
.bookmark-remove:hover { color: var(--heading); }

/* ─── Sample chapter reader ─────────────────────────────────────────── */

/* Breadcrumb + pager (replaces the old sample-shell/sample-sidebar TOC —
   the app-shell sidebar, already present via app_header, is the
   persistent nav now; a second TOC sidebar here just duplicated it). */
.breadcrumb {
  font: 400 13px 'Lato';
  color: var(--muted);
  margin-bottom: 20px;
}
.breadcrumb a { color: var(--muted); text-decoration: underline; }
.breadcrumb a:hover { color: var(--heading); }
.breadcrumb-sep { margin: 0 8px; color: var(--border); }

.chapter-pager {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  margin: 24px 0;
  padding: 16px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  font-family: 'Lato', sans-serif;
  font-weight: 700;
}
.chapter-pager a { color: var(--heading); }
.chapter-pager-next { margin-left: auto; text-align: right; }

/* "View Slides" row in .chapter-hero (course_section.html) — the same
   trigger app_header's sidebar already has, surfaced right in the top
   nav bar too so it's not something a reader has to go find in the
   sidebar first. .chapter-hero .wrapper is a wrapping flexbox holding
   breadcrumb + this row + the prev/next pager as three siblings; without
   a fixed order/flex-basis here, "space-between" placed this row wherever
   the wrap happened to land, which shifted with the length of the
   prev/next link text. flex-basis:100% forces it onto its own full-width
   line, and order:3 keeps it there below breadcrumb/pager regardless of
   DOM position or how much room those two take up. */
.chapter-hero-slides-row { flex-basis: 100%; order: 3; margin-top: 12px; }
.chapter-hero-slides-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: 20px;
  background: rgba(255,255,255,0.12);
  color: #fff;
  font-family: 'Lato', sans-serif;
  font-weight: 700;
  font-size: 13px;
  text-decoration: none;
}
.chapter-hero-slides-link:hover { background: rgba(255,255,255,0.22); }

/* Chapter hero — same slide-bg surface as .home-hero/.app-shell-sidebar,
   compact, holding the breadcrumb and prev/next pager above the prose
   rather than as plain text rows in the white content area. Light-on-dark
   variants of .breadcrumb/.chapter-pager rather than new markup, so
   book.go/course.go's existing "breadcrumb"/"book_pager"/"course_pager"
   partials can render into it unchanged. */
.chapter-hero {
  background: var(--header-bg) url('/images/hero-bg.jpg') center top no-repeat;
  background-size: cover;
  /* Missing this was the bug: background-position/-size for a plain
     (scroll) attachment are resolved against THIS element's own box, a
     short wide strip — a different crop/scale than .app-shell-sidebar's
     tall narrow box right next to it, even though both reference the
     same image at "center top". background-attachment: fixed (same as
     .app-shell-sidebar/.home-hero) resolves position/size against the
     viewport instead for both, so the same slice of the image lines up
     across the sidebar/hero boundary instead of looking like two
     separately-cropped copies. */
  background-attachment: fixed;
  padding: 28px 20px;
}
@media (min-width: 721px) {
  /* Sticky on desktop only — same technique as .app-shell-sidebar (the
     nearest scrolling ancestor is the viewport itself, no overflow set
     on .app-shell-main in between, so this works the same way): the
     breadcrumb/pager stay visible while reading a long section instead
     of scrolling away with the first screenful. Not on mobile, where
     the hero's own vertical space is worth reclaiming once scrolled
     past, and a sticky top bar there would compete with the OS chrome. */
  .chapter-hero {
    position: sticky;
    top: 0;
    z-index: 10;
  }
}
.chapter-hero .wrapper {
  /* Deliberately NOT narrowed like .home-hero's own wrapper — this hero
     sits directly above the white content area's wider .container
     .wrapper, so the breadcrumb needs the same max-width (inherited from
     the base .wrapper rule) to left-align under the same margin as the
     kicker/h1 below it, instead of centering a narrower column and
     drifting off to the right of them. */
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}
.chapter-hero .breadcrumb {
  color: rgba(255,255,255,0.7);
  font-weight: 700;
  margin-bottom: 0;
}
.chapter-hero .breadcrumb a { color: rgba(255,255,255,0.9); }
.chapter-hero .breadcrumb a:hover { color: #fff; }
.chapter-hero .breadcrumb-sep { color: rgba(255,255,255,0.35); }
.chapter-hero .chapter-pager {
  margin: 0;
  padding: 0;
  border: none;
  flex: 0 0 auto;
}
.chapter-hero .chapter-pager a { color: #fff; }
.chapter-hero .chapter-pager a:hover { color: var(--accent); }
.chapter-hero .chapter-pager span { display: none; }
.chapter-hero + .container { padding-top: 40px; }
@media (max-width: 720px) {
  /* One line per entry instead of side-by-side — side-by-side has no
     room to wrap a long title on a narrow screen without the prev/next
     pair colliding. Each still keeps its own alignment (prev left, next
     right) and is capped at 80% width so a long title wraps onto a
     second line instead of running edge to edge. */
  .chapter-hero .chapter-pager {
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    gap: 8px;
  }
  .chapter-hero .chapter-pager-prev,
  .chapter-hero .chapter-pager-next {
    max-width: 80%;
    margin: 0;
  }
  .chapter-hero .chapter-pager-prev { align-self: flex-start; text-align: left; }
  .chapter-hero .chapter-pager-next { align-self: flex-end; text-align: right; }
}

.chapter-complete {
  margin: 32px 0;
  padding-top: 24px;
  border-top: 1px solid var(--border);
  font-family: 'Lato', sans-serif;
}
.chapter-complete-note { color: var(--success); font-weight: 700; margin: 0; }

/* Module slide pane (internal/slides) — a Go/HTML reimplementation of
   sqli's slides.yaml -> Beamer PDF renderer, sitting above the booklet
   content on a course module page. Reuses the real theme's own two
   background images (taop-courses/beamer/beamerthemeTAOP.sty,
   beamer/img/TAOP_Keynote_2018_*_1610.png, copied to public/images/) and
   its taopText color (#67527A — already this app's own --body-text) —
   not a dark-purple/white-text reinterpretation like an earlier version
   of this file had. The real theme only ever swaps to ONE alternate
   background (\taopsetsectionbg, section dividers); every other frame
   type (overview/concept/query/diagram/latex — "Normal", "two-col" etc.
   are all layouts on the SAME background, not different backgrounds)
   uses its default \setbeamertemplate{background}. .slide-viewport:has()
   picks the divider background exactly the same way — one rule, no
   per-type background overrides to keep in sync. */
.slide-pane { margin: 0 0 32px 0; border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
.slide-viewport {
  position: relative;
  background: #fff url('/images/slide-bg-normal.png') center / cover no-repeat;
  min-height: 320px;
  display: flex;
  align-items: center;
  padding: 40px 48px;
  font-family: 'Lato', sans-serif;
  color: var(--body-text);
}
/* :has() must be scoped to the currently-visible slide
   (.slide-modal-slide:not([hidden])), not just "any descendant anywhere"
   — every slide in the deck lives in the DOM at once (partials.html's
   slide_modal renders all of them, JS just toggles [hidden]), so an
   unscoped :has(.slide-divider) would match permanently the moment ANY
   slide in the whole deck is a divider, painting every other slide with
   the divider background too. */
.slide-viewport:has(.slide-modal-slide:not([hidden]) .slide-divider) {
  background-image: url('/images/slide-bg-divider.png');
}
/* Deck-opening title card (slides.TitleSlide) — dark purple cover
   background, matching beamerthemeTAOP.sty's \taoptitleframe
   (\taopsetdarkbg + \titlepage). The only other slide type with its own
   background swap (section dividers, above) — everything else shares
   the light "normal" background. */
.slide-viewport:has(.slide-modal-slide:not([hidden]) .slide-titlecard) {
  background-image: url('/images/slide-bg-cover.png');
}
.slide-titlecard {
  width: 100%;
  text-align: center;
}
.slide-titlecard-course {
  font-family: 'Lato', sans-serif;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: rgba(255,255,255,0.7);
  margin: 0 0 14px 0;
}
.slide-titlecard-module {
  font-family: 'Bookman', Georgia, serif;
  font-size: 34px;
  color: #fff;
  margin: 0;
}
.slide-frame { width: 100%; }
/* overview/concept slides (title + bullets, no columns) read as a single
   centered block on a presentation slide — the frame itself centers its
   direct children (title, bullet list, note), but text INSIDE the bullet
   list stays left-aligned (a centered bullet list with centered text
   reads badly — ragged left edges per line). .slide-columns overrides
   back to left-aligned since side-by-side columns are their own layout,
   not a single centered block. */
.slide-overview, .slide-concept { text-align: center; }
.slide-title {
  font-family: 'Bookman', Georgia, serif;
  font-size: 28px;
  color: var(--body-text);
  margin-bottom: 20px;
}
.slide-bullets {
  list-style: none;
  padding: 0;
  margin: 0 auto;
  width: fit-content;
  max-width: 100%;
  text-align: left;
}
.slide-bullets li {
  position: relative;
  padding-left: 22px;
  margin-bottom: 12px;
  font-size: 18px;
  line-height: 1.5;
  color: var(--body-text);
}
.slide-bullets li::before { content: '\2022'; position: absolute; left: 4px; color: var(--accent); }
.slide-bullets strong, .slide-note strong { color: var(--heading); }
.slide-bullets code, .slide-note code {
  background: var(--panel-alt);
  color: var(--accent);
  padding: 1px 6px;
  border-radius: 3px;
  font-size: 0.9em;
}
.slide-note { color: var(--body-text); font-size: 17px; line-height: 1.6; margin-top: 16px; }

/* Shorter than the single-column reading measure (68ch, see .slide-query
   .code-block below) — two columns side by side need less width each to
   stay comfortable, not the same total width as a single column of text. */
.slide-columns { display: flex; gap: 32px; margin: 8px auto 0 auto; max-width: 56ch; text-align: left; }
.slide-column { flex: 1; min-width: 0; }
.slide-column h3 { font: 700 15px 'Lato'; color: var(--accent); margin-bottom: 8px; }
.slide-column p, .slide-column { font-size: 16px; line-height: 1.55; color: var(--body-text); }

.slide-divider {
  justify-content: center;
  text-align: center;
  min-height: 320px;
}
.slide-divider span {
  font-family: 'Bookman', Georgia, serif;
  font-size: 30px;
  color: var(--body-text);
}

/* align-items: center keeps every child (title, notes, code block, figure)
   centered on the slide instead of stretching full-width/flush-left — same
   "don't use the full slide width" treatment title/content slides already
   get via .slide-overview/.slide-concept's text-align: center above. */
.slide-diagram, .slide-query, .slide-latex { display: flex; flex-direction: column; align-items: center; }
.slide-diagram .slide-note, .slide-query .slide-note, .slide-latex .slide-note { max-width: 68ch; }
.slide-figure { margin: 8px 0 0 0; text-align: center; }
.slide-figure img { max-width: 100%; max-height: 55vh; background: #fff; border-radius: 4px; }
.slide-figure figcaption { font-size: 13px; color: var(--muted); margin-top: 10px; font-style: italic; }
.slide-figure-pending {
  color: var(--muted);
  font-size: 14px;
  font-style: italic;
  padding: 20px;
  border: 1px dashed var(--border);
  border-radius: 4px;
  text-align: center;
}

/* .code-block's own "width: fit-content" (a shrink-to-fit computation)
   resolves to 0 the first time this element is laid out right as its
   .slide-modal-slide ancestor's [hidden] attribute is removed (slide
   navigation) — confirmed with a real screenshot, not just
   getComputedStyle: still 0 with a higher-specificity non-!important
   override of the exact same width (100% or auto), only fixed by
   !important. Whatever the underlying engine quirk (a hidden-to-visible
   transition + shrink-to-fit interaction, reproduced consistently), a
   plain override isn't enough here — !important is load-bearing, not
   decorative. */
.slide-query .code-block, .slide-latex .code-block { margin-top: 12px; width: 100% !important; max-width: 68ch !important; }
.slide-kw { color: var(--accent); }

.slide-table { border-collapse: collapse; margin: 12px auto 0 auto; font: 14px 'JetBrains Mono', monospace; color: var(--body-text); }
.slide-table td { padding: 6px 14px; border-bottom: 1px solid var(--border); }
.slide-row-purple td { background: rgba(96,178,211,0.15); }
.slide-row-red td { background: rgba(226,75,74,0.12); }
.slide-row-rule td { border-top: 1px solid var(--body-text); }

.slide-unknown { color: var(--muted); font-style: italic; }

/* Module TOC page: the "Sections in this Module" list alongside a live
   thumbnail of the deck's own first (title) slide — a real render of
   internal/slides' own markup at design size, scaled down with a CSS
   transform, not a static screenshot/placeholder image. Two-column grid
   on desktop only (TOC | slide thumbnail); below the breakpoint it's a
   single column with the TOC first, slides second — the DOM order below
   already puts the list before the thumbnail, so stacking needs no
   reordering. */
.module-toc-row { display: grid; grid-template-columns: minmax(0, 1fr) 480px; gap: 40px; align-items: start; }
.module-toc-list { min-width: 0; }
.module-toc-slide-thumb {
  position: relative;
  width: 100%;
  max-width: 480px;
  aspect-ratio: 900 / 562;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--border);
  display: block;
  text-decoration: none;
}
.module-toc-slide-thumb-stage {
  width: 900px;
  height: 562px;
  transform: scale(0.53333);
  transform-origin: top left;
  background: #fff url('/images/slide-bg-cover.png') center / cover no-repeat;
  display: flex;
  align-items: center;
  padding: 40px 48px;
  font-family: 'Lato', sans-serif;
  box-sizing: border-box;
}
.module-toc-slide-thumb-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: rgba(20,10,35,0.35);
  color: #fff;
  font: 700 13px 'Lato';
  letter-spacing: 0.5px;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.module-toc-slide-thumb:hover .module-toc-slide-thumb-overlay { opacity: 1; }
@media (max-width: 900px) {
  .module-toc-row { grid-template-columns: 1fr; }
  .module-toc-slide-thumb { max-width: 100%; }
}

.slide-pane-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--panel-alt);
  padding: 10px 20px;
  font-family: 'Lato', sans-serif;
}
.slide-nav-btn { font: 700 13px 'Lato'; color: var(--heading); }
.slide-nav-btn-disabled { color: var(--muted); pointer-events: none; }
.slide-counter { font-size: 13px; color: var(--muted); }
/* Overlay prev/next, layered on the slide image itself — a faster jump
   than reaching for .slide-pane-controls below the slide, which stays as-
   is for the counter + a discoverable full-width target. Vertically
   centered on the left/right edges rather than pinned to the top corners
   (an earlier version was): in the "View Slides" modal (the only place
   this renders now — see module_slide_modal), top-right collided with
   .slide-modal-close sitting in that exact corner, leaving Close as the
   only clickable thing there. Centered avoids that entirely, and reads
   as the more standard fullscreen-viewer control placement anyway. */
.slide-overlay-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(0,0,0,0.28);
  color: #fff;
  font-size: 14px;
  transition: background .15s;
}
.slide-overlay-nav:hover { background: rgba(0,0,0,0.5); }
.slide-overlay-nav-prev { left: 14px; }
.slide-overlay-nav-next { right: 14px; }

/* Reading typography: Bookman is a display/heading face (matches the print
   cover and chapter titles) — too heavy and wide for extended body copy, so
   paragraphs use a standard book-serif stack instead. Nav chrome, the TOC,
   the index, and figure captions all stay in the site's own Lato (see the
   rules below and in the TOC/index/figure-list sections) rather than either
   serif, so UI furniture reads as "this app" and prose reads as "this book". */
.book-prose {
  font-family: Georgia, 'Iowan Old Style', Palatino, 'Book Antiqua', serif;
  font-size: 17px;
  line-height: 1.75;
  color: var(--ink);
}
.book-prose h1, .book-prose h2, .book-prose h3, .book-prose h4 { font-family: 'Bookman', Georgia, serif; }
/* The global `a` rule (near the top of this file) colors links the same
   as body text, relying on :hover alone to reveal them — invisible until
   the reader happens to mouse over one. Book prose links (footnote-style
   external references, internal cross-references) need to read as links
   at rest. */
.book-prose a { color: var(--accent); text-decoration: underline; }
.book-prose a:hover { color: var(--ink); }
.book-prose h1 { font-size: 30px; }
.book-prose h2 { font-size: 23px; margin-top: 36px; }
.book-prose h3 { font-size: 18px; margin-top: 28px; }
.book-prose h4 { font-size: 16px; font-style: italic; margin-top: 24px; }

/* "Smart" LaTeX/booktabs-style tables (top+bottom rule, header rule, no
   vertical lines at all) — same look as tapoueh.org's `.prose table`,
   used for the handful of course sections written as Pandoc "simple
   tables" (see internal/booklet's convertSimpleTables). */
.book-prose table {
  border-collapse: collapse;
  width: auto;
  max-width: 68ch;
  margin: 1.8rem 0;
  font-size: 15px;
  line-height: 1.5;
}
.book-prose thead th {
  border-top: 1.5px solid var(--ink);
  border-bottom: .5px solid var(--ink);
  font-family: 'Bookman', Georgia, serif;
  font-weight: 600;
  color: var(--ink);
  text-align: left;
  padding: 0.4rem 1rem 0.4rem 0;
  white-space: nowrap;
}
.book-prose thead th:first-child { padding-left: 0; }
.book-prose tbody td {
  padding: 0.3rem 1rem 0.3rem 0;
  vertical-align: top;
  border: none;
}
.book-prose tbody td:first-child { padding-left: 0; }
.book-prose tbody tr:last-child td {
  border-bottom: 1.5px solid var(--ink);
  padding-bottom: 0.5rem;
}

/* The actual reading measure: ~68 characters/line, applied to running text
   only. Code blocks, figures, tables, and the ToC/index (already full-width
   in their own rules) are deliberately NOT constrained here — a 68ch cap on
   a `select ...` block or a data table would just force more wrapping and
   hurt readability instead of helping it. h4 is here too — interviews use
   it for the (often long) question being asked, which was stretching to
   the full white content column instead of the same measure as the answer
   below it. */
.book-prose > p, .book-prose > ul, .book-prose > ol, .book-prose > blockquote, .book-prose > .taopquote, .book-prose > h4 {
  max-width: 68ch;
}
/* `ch` is defined by the *font's* "0" glyph width, not its average character
   width. Lato's "0" is proportionally wide next to its average letter, so
   68ch of Lato renders ~85-90 real characters/line instead of ~68 — verified
   in-browser at iPad-landscape widths (1180px), where this was visibly too
   long. 52ch of Lato measures out to the same real reading width as 68ch of
   the Georgia book-prose stack. */
/* .sample-subtitle is itself a <p class="sample-subtitle"> direct child of
   .book-prose, so it also matches `.book-prose > p` above — and that
   selector (class + type = specificity 0,1,1) outranks a bare
   `.sample-subtitle` (0,1,0) regardless of source order, so the 68ch rule
   was silently winning here. Matching the combinator brings this to 0,2,0,
   which wins. */
.book-prose > .sample-subtitle, .sample-concept-list {
  max-width: 52ch;
}
.book-prose p, .book-prose li { font-size: 17px; line-height: 1.75; }
.book-prose ul, .book-prose ol { padding-left: 22px; margin: 0 0 16px 0; }
.book-prose blockquote {
  margin: 24px 0;
  padding: 4px 0 4px 22px;
  border-left: 3px solid var(--body-text);
}
.book-prose blockquote p {
  font-style: italic;
  color: var(--body-text);
  max-width: none; /* already constrained by the parent blockquote */
}
.book-prose blockquote p.cite { font-style: normal; font-family: 'Lato', sans-serif; font-size: 14px; color: var(--muted); }

/* "::: taopquote" fenced div (taop-vol-1/pandoc/filters/taopquote.lua) —
   interview pull-quotes. book.latex's own \begin{taopquote} is a \quote
   (indented) environment with a 32pt \textcolor{black!15}{"} hanging in
   the left margin via \makebox[0pt][r]; the web has no page margin to
   hang into, so the same giant light-gray opening quote sits inside the
   block's own left indent instead of overflowing further left. */
.taopquote {
  position: relative;
  margin: 28px 0;
  padding-left: 44px;
}
.taopquote::before {
  content: '\201C';
  position: absolute;
  left: 0;
  top: -0.18em;
  font-family: Georgia, 'Iowan Old Style', Palatino, 'Book Antiqua', serif;
  font-size: 64px;
  line-height: 1;
  color: rgba(0,0,0,0.15);
}
.taopquote > p:last-child { margin-bottom: 0; }

/* Justified text reads well once a line is long enough to avoid ugly rivers
   of whitespace — desktop only, and only worthwhile now that paragraphs
   sit at the ~68-character measure above. */
@media (min-width: 900px) {
  .book-prose p { text-align: justify; hyphens: auto; }
}

.sample-kicker {
  font: 700 14px 'Lato';
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 6px;
}
.sample-subtitle { font-family: 'Lato', sans-serif; color: var(--muted); font-size: 17px; }

/* Home page ("/") hero — both logged-out (login/create-account form) and
   logged-in ("welcome back") variants share this, see dashboard.html/
   dashboard_logged_out.html. Specifically the SAME hero as
   theartofpostgresql.com/book/contents/ (taop.xyz's
   layouts/page/contents.html: .cp-hero) — same background image
   (images/demo/slide.jpg, copied to public/images/hero-bg.jpg), same dark
   ink overlay/fixed attachment, same stats-bar-under-the-subtitle pattern. */
.home-hero {
  background: var(--header-bg) url('/images/hero-bg.jpg') center top no-repeat;
  background-size: cover;
  background-attachment: fixed;
  min-height: 50vh;
  display: flex;
  align-items: center;
  text-align: center;
  padding: 110px 20px 60px 20px;
}
.home-hero .wrapper { max-width: 720px; }
.home-hero-kicker {
  font: 700 13px 'Lato';
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 14px;
}
.home-hero h1 { color: #fff; font: 400 44px 'Bookman'; margin-bottom: 18px; }
.home-hero-subtitle { color: var(--muted-on-dark); font: 300 18px 'Lato'; line-height: 1.6; margin-bottom: 0; }
.home-hero-subtitle a { color: #fff; text-decoration: underline; }
.home-hero-stats {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0;
  max-width: 560px;
  margin: 32px auto 0 auto;
}
.home-hero-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 28px;
  border-right: 1px solid rgba(255,255,255,0.2);
}
.home-hero-stat:last-child { border-right: none; }
.home-hero-stat-n { font: 700 32px 'Lato'; color: var(--accent); letter-spacing: -0.02em; }
.home-hero-stat-l {
  font: 300 12.5px 'Lato';
  color: rgba(255,255,255,0.65);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-top: 6px;
  white-space: nowrap;
}
.home-hero-cta { margin-top: 32px; display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; }
.home-hero .button.button_transparent,
.home-hero .button.button_transparent:visited { border-color: rgba(255,255,255,0.5); color: rgba(255,255,255,0.9); }
.home-hero .button.button_transparent:hover { background: rgba(255,255,255,0.12); border-color: #fff; color: #fff; }

/* Logged-out home hero embeds the magic-link form directly (see
   dashboard_logged_out.html) rather than just linking to /login — label
   color overridden for the dark hero background (the default `label`
   rule assumes a light card). */
.home-hero-login { max-width: 360px; margin: 32px auto 0 auto; text-align: center; }
.home-hero-login label { color: rgba(255,255,255,0.75); text-align: center; }
.home-hero-login .flash { text-align: left; }

/* Logged-in dashboard's "welcome back" hero — same background/typography
   as the logged-out hero, just shorter (no stats bar, no form) since it's
   a banner above the real dashboard content below, not the whole page. */
.home-hero-compact { min-height: 0; padding: 110px 20px 40px 20px; }
.home-hero-compact h1 { font-size: 32px; margin-bottom: 0; }
.home-hero + .container { padding-top: 60px; }

/* Course module TOC hero (course_module_toc.html) — same home-hero
   surface as course_detail's, plus a breadcrumb row at the top,
   light-on-dark like .chapter-hero's own breadcrumb (that class isn't
   used here since this hero is the taller home-hero-compact shape, not
   the slim breadcrumb-only strip). No up/next pager on this hero — the
   course's own module grid (course_detail.html) and each section's own
   pager (course_section.html) already cover that navigation; repeating
   it here was redundant with the breadcrumb right above it. */
.home-hero .breadcrumb { color: rgba(255,255,255,0.7); font-weight: 700; margin-bottom: 24px; }
.home-hero .breadcrumb a { color: rgba(255,255,255,0.9); }
.home-hero .breadcrumb a:hover { color: #fff; }
.home-hero .breadcrumb-sep { color: rgba(255,255,255,0.35); }
.module-hero-stats { font-size: 14px; }
/* Larger kicker on this page only — "{Course} — Module N" reads as a
   secondary heading here, not the usual one-line eyebrow tag. */
.module-hero .home-hero-kicker { font-size: 16px; letter-spacing: 1px; }

/* "View Slides" modal (partials.html's module_slide_modal, driven by
   slide-modal.js) — almost full-screen, every slide pre-rendered and
   hidden in the markup, JS just toggles which one shows. */
.slide-modal-overlay {
  display: none;
  position: fixed;
  inset: 20px;
  z-index: 2000;
  background: var(--panel-bg);
  border-radius: 10px;
  box-shadow: 0 10px 60px rgba(0,0,0,0.4);
  flex-direction: column;
  overflow: hidden;
}
.slide-modal-overlay.slide-modal-open { display: flex; }
.slide-modal-stage { position: relative; flex: 1; min-height: 0; display: flex; }
/* align-items stays the base .slide-viewport's "center" here — a plain
   align-items: flex-start (an earlier version of this rule) top-aligned
   EVERY slide, not just tall ones, which is why title/bullet slides
   rendered pinned to the top-left instead of centered. The real fix for
   tall query slides (SQL + output can exceed the modal's fixed height,
   and centering a flex item taller than its scrollable container clips
   both ends with no way to scroll to them — scrollTop can't go negative)
   is the "auto margin" trick below instead: margin:auto on the slide
   itself centers it when it fits and safely collapses to top-aligned +
   scrollable when it doesn't, so short and tall slides both work without
   picking one behavior for all of them. */
.slide-modal-stage .slide-viewport { flex: 1; min-height: 0; height: 100%; overflow-y: auto; }
.slide-modal-slide { width: 100%; margin: auto 0; }
.slide-modal-close {
  position: absolute;
  top: 10px;
  right: 14px;
  z-index: 3;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: rgba(0,0,0,0.15);
  color: var(--heading);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.slide-modal-close:hover { background: rgba(0,0,0,0.28); }

/* Two-card Book/Courses intro on the logged-out home page — what each
   product is and what its free sample covers, at a glance, before the
   footer. Short by design: one description line + one sample line each,
   no CTA (the hero's email form above is the only action on this page). */
.home-intro { padding: 56px 0; }
.home-intro-heading { text-align: center; max-width: 640px; margin: 0 auto 40px auto; }
.home-intro-heading-subtitle { color: var(--muted); font: 300 20px 'Lato'; margin-bottom: 12px; }
.home-intro-heading p:last-child { font-size: 15px; }
.home-intro-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 24px;
}
.home-intro-card {
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 28px;
  text-align: center;
}
.home-intro-card i { font-size: 28px; color: var(--accent); margin-bottom: 12px; }
.home-intro-card h3 { margin-bottom: 10px; }
.home-intro-sample { font-size: 14px; color: var(--muted); margin-bottom: 0; }
.home-intro-sample strong { color: var(--heading); }
.home-intro-sample-detail {
  font-size: 13px;
  color: var(--muted);
  line-height: 1.6;
  margin: 12px 0 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* "One System, One Learning Path" — same section as theartofpostgresql.com's
   homepage (themes/taop/layouts/partials/index/system.html), same class
   shapes under this app's own naming (.home-system-*) rather than the
   marketing site's (.index_system/.system_*). */
.home-system { padding: 80px 0; background: var(--panel-bg); }
.home-system-heading { text-align: center; margin: 0 auto 0 auto; max-width: 740px; }
.home-system-tagline { color: var(--muted); font: 300 22px 'Lato'; margin-top: 8px; }
.home-system-flow {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 20px;
  margin: 40px auto;
  max-width: 900px;
}
.home-system-item { text-align: center; padding: 20px; min-width: 120px; }
.home-system-icon { font-size: 36px; display: block; margin-bottom: 10px; color: var(--accent); }
.home-system-item h6 { color: var(--heading); font: 600 16px 'Lato'; margin: 0 0 5px 0; }
.home-system-item p { color: var(--body-text); font-size: 13px; margin: 0; }
.home-system-arrow { color: var(--muted); font-size: 24px; }
.home-system-cta { text-align: center; margin-top: 30px; }
.home-system-note { text-align: center; margin-top: 32px; font: 500 20px 'Bookman', Georgia, serif; color: var(--heading); }
@media (max-width: 720px) {
  .home-system-flow { gap: 8px; }
  .home-system-arrow { transform: rotate(90deg); }
}

.sample-downloads-section, .sample-short-toc { text-align: center; margin: 48px 0; }
.sample-downloads-section { max-width: 560px; margin-left: auto; margin-right: auto; }
.sample-downloads-section p { color: var(--muted); margin-bottom: 20px; }
.sample-downloads-buttons { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }

/* "What's inside" on the sample landing page — richer than a plain link
   list: a numbered card per part with a one-line teaser, same "included"
   tag on every card since every part listed here does have a chapter in
   the sample (the List of Figures/Index cards use an emoji instead of a
   number, same visual language). */
.toc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 16px;
  margin-top: 28px;
  text-align: left;
}
.toc-card {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 18px 20px;
  transition: box-shadow .15s, border-color .15s;
}
.toc-card:hover { box-shadow: 0 4px 16px rgba(55,38,73,0.1); border-color: var(--accent); }
.toc-card-n {
  flex-shrink: 0;
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-alt);
  color: var(--accent);
  font: 700 15px 'Lato';
  border-radius: 50%;
}
.toc-card-body { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.toc-card-title { font: 700 15px 'Lato'; color: var(--heading); }
.toc-card-teaser { font-family: 'Lato', sans-serif; font-size: 13px; color: var(--muted); line-height: 1.4; }
.toc-card-tag {
  flex-shrink: 0;
  align-self: center;
  font: 700 10px 'Lato';
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--success);
  background: #eef7ee;
  padding: 4px 10px;
  border-radius: 10px;
}
.toc-card-tag.toc-card-tag-sample { color: var(--accent); background: #eef7fa; }
.toc-card-tag.toc-card-tag-locked { color: var(--muted); background: var(--panel-alt); display: inline-flex; align-items: center; gap: 5px; }
.toc-card-tag.toc-card-tag-locked i { font-size: 9px; }
/* 2026-edition chapter tags (book_home.html, Chapter.Update2026) — "New"
   for a chapter that didn't exist in the previous edition, "Updated" for
   one with a confirmed substantive content addition (new subsection, new
   appendix chapter) — see the manifest field's own doc comment for the
   real source (theartofpostgresql.com/book/updates/). */
.toc-card-tag.toc-card-tag-new { color: #fff; background: var(--accent); }
.toc-card-tag.toc-card-tag-updated { color: var(--heading); background: #f3e9d2; }
/* Per-chapter PostgreSQL version-coverage tags (Chapter.PGVersions) — one
   badge per major version the chapter has a confirmed dedicated section
   covering, matching theartofpostgresql.com/book/updates/'s own "PG NN"
   pill styling (#60b2d3). */
.toc-card-tag.toc-card-tag-pg { color: #fff; background: #60b2d3; }
.book-updates-link { font-size: 13px; margin-top: 18px; }
.book-updates-link a { color: #fff; text-decoration: underline; }
.book-updates-link .toc-card-tag { font-size: 9px; padding: 2px 7px; vertical-align: 1px; }

/* Book home (/book) — vertical timeline, one row per part: a rail column
   (dot + connecting line) beside the part's own header and its full
   chapter-level TOC, rather than the old flat toc-grid of part-only
   cards. Each row supplies its own line segment (hidden on the last row)
   instead of one long absolutely-positioned line, so the rail never has
   to be measured against variable per-part chapter-list height. */
.book-timeline { margin-top: 28px; }
.book-timeline-row { display: flex; gap: 24px; }
.book-timeline-rail { display: flex; flex-direction: column; align-items: center; width: 20px; flex-shrink: 0; }
.book-timeline-dot {
  width: 16px; height: 16px;
  margin-top: 6px;
  border-radius: 50%;
  background: var(--panel-bg);
  border: 3px solid var(--heading);
  flex-shrink: 0;
}
.book-timeline-dot.book-timeline-dot-full { background: var(--accent); border-color: var(--accent); }
.book-timeline-line { flex: 1; width: 2px; background: var(--border); margin: 4px 0; }
.book-timeline-row:last-child .book-timeline-line { display: none; }
.book-timeline-content { flex: 1; padding-bottom: 32px; }
.book-timeline-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.book-timeline-title { font: 700 20px 'Bookman', serif; color: var(--heading); }
.book-timeline-title:hover { color: var(--accent); }
/* Chapter/section numbers matching the print book's own numbering
   (content.BookManifest.Nav — see its doc comment for where the numbers
   come from) — muted so the title stays the visual lead. */
.chapter-number { color: var(--muted); font-weight: 400; margin-right: 2px; }
.book-timeline-chapters {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px 32px;
}
@media (max-width: 720px) {
  .book-timeline-chapters { grid-template-columns: 1fr; }
}
.book-timeline-chapters li {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px dashed var(--border);
  font-size: 14px;
}
.book-timeline-chapters a { color: var(--ink); }
.book-timeline-chapters a:hover { color: var(--accent); }

/* Part page (book_part.html) and chapter TOC page (book_chapter_toc.html)
   — same timeline rail as book_home.html, one row per chapter/section
   instead of one row per part; no nested chapter list, so rows are
   shorter. Indented from the page's left margin (rather than flush with
   the H1 above it) and set in a lighter regular-weight Lato instead of
   the part timeline's bold Bookman serif — a sub-list one level down
   reads as one, not a repeat of the same heading weight. */
.book-timeline-chapter-list { margin-left: 32px; }
.book-timeline-chapter-list .book-timeline-content { padding-bottom: 20px; }
.book-timeline-chapter-list .book-timeline-header { margin-bottom: 0; }
.book-timeline-chapter-list .book-timeline-title { font: 400 16px 'Lato', sans-serif; }

/* Chapter TOC page (book_chapter_toc.html) — separates the section list
   above from the chapter's own opening prose below it, rather than the
   two running together with only whitespace between them. */
.chapter-toc-divider { border: none; border-top: 1px solid var(--border); margin: 36px 0 28px 0; }
.book-timeline-chapter-link {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  width: 100%;
  gap: 12px;
}
.chapter-sample-tag {
  font: 700 9px 'Lato';
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--accent);
  background: #eef7fa;
  padding: 2px 7px;
  border-radius: 8px;
}

/* Book part-level landing page (book_part.html) — .book-timeline dot+rail
   chapter TOC (courses_home.html/course_detail.html use the LMS-style
   card grid below instead, not this). */
.part-progress-note { color: var(--muted); font-size: 14px; margin-bottom: 20px; }
.part-chapter-check { color: var(--success); margin-right: 6px; }

.sample-parts-list {
  list-style: none;
  padding: 0;
  margin: 24px auto 0 auto;
  max-width: 480px;
  font-family: 'Lato', sans-serif;
  text-align: left;
}
.sample-parts-list li { border-bottom: 1px solid var(--border); }
.sample-parts-list a {
  display: block;
  padding: 14px 4px;
  color: var(--ink);
  font-weight: 600;
}
.sample-parts-list a:hover { color: var(--accent); }
.sample-parts-list-label { display: block; padding: 14px 4px; color: var(--ink); font-weight: 600; }

.sample-get-started { text-align: center; margin: 48px 0 20px 0; }

/* Courses catalog (courses_home.html) and one course's own module grid
   (course_detail.html) — an LMS-style "bundle of N courses" grid instead
   of the book's TOC-style timeline, per your steer: each course/module
   is its own card, thumbnailed like a slide (same light .slide-viewport
   background/typography the module slide pane itself uses — see the
   "Module slide pane" section above), not a list row. 3 columns on
   desktop, collapsing at the same breakpoints as the marketing site's
   own course-overview.html grid (960px, 720px). */
/* flex, not grid — a plain CSS grid left a short last row (module counts
   that aren't a multiple of 3, e.g. 8 modules = rows of 3/3/2) flush to
   the left instead of centered. flex-wrap + justify-content: center
   handles that for free (same technique .course-tier-row already uses
   for the same reason), so each card gets an explicit flex-basis instead
   of an implicit grid-column 1fr share. */
.course-card-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 24px;
  margin: 28px 0;
}
.course-card-grid .course-slide-card { flex: 0 1 calc((100% - 48px) / 3); }
@media (max-width: 960px) { .course-card-grid .course-slide-card { flex-basis: calc((100% - 24px) / 2); } }
@media (max-width: 720px) { .course-card-grid .course-slide-card { flex-basis: 100%; } }
.course-slide-card {
  display: block;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  text-decoration: none;
  transition: box-shadow .18s, transform .18s;
}
.course-slide-card:hover { box-shadow: 0 4px 20px rgba(55,38,73,0.12); transform: translateY(-2px); }
.course-slide-thumb {
  background: #fff url('/images/slide-bg-normal.png') center / cover no-repeat;
  min-height: 130px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 22px;
  text-align: center;
}
.course-slide-thumb i { font-size: 30px; color: var(--accent); }
.course-slide-thumb h3 { font: 700 16px 'Bookman', serif; color: var(--heading); margin: 0; line-height: 1.3; }
/* /courses catalog cards: the deck's own dark purple cover/title
   background (TAOP_Keynote_2018_Cover_1610.png — the theme's one truly
   dark background; "Title"/"Normal" are both near-white, only the cover
   slide itself uses ink) instead of the "Normal" one used elsewhere —
   this grid is a list of course NAMES, closer to a deck's own cover
   slide than to a "Normal" content slide. White-on-dark text/icon to
   match (--heading reads as near-invisible on this background). */
.course-slide-thumb-dark {
  background-image: url('/images/slide-bg-cover.png');
}
.course-slide-thumb-dark i { color: var(--accent); }
.course-slide-thumb-dark h3 { color: #fff; }
.course-module-num { font: 700 22px 'Lato'; color: var(--accent); letter-spacing: -0.02em; }
.course-slide-info { padding: 16px 20px 20px 20px; }
.course-slide-tagline { font-size: 13.5px; color: var(--muted); line-height: 1.5; margin-bottom: 14px; }
.course-slide-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font: 700 11px 'Lato';
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 10px;
}
.course-slide-progress { font-size: 13px; color: var(--body-text); font-weight: 700; }

/* Course detail page (course_detail.html) — the course's own hero,
   same thumbnail language as its module cards below, just larger and
   not a link. */
.course-hero-card {
  display: flex;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  margin: 24px 0 32px 0;
}
.course-hero-thumb { flex: 0 0 42%; min-height: 180px; }
.course-hero-thumb i { font-size: 40px; }
.course-hero-thumb h1 { font: 700 24px 'Bookman', serif; color: var(--heading); margin: 0; line-height: 1.3; }
.course-hero-card .course-slide-info { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 24px 28px; }
.course-hero-card .course-slide-tagline { font-size: 15px; margin-bottom: 16px; }
.course-booklet-download { margin-top: 16px; align-self: flex-start; }
/* Module TOC page's own downloads (handout PDF, SQL bundle) — a plain
   row above the slide pane/section list, not folded into the hero. */
.module-download-row { display: flex; gap: 12px; flex-wrap: wrap; margin: 16px 0 28px 0; }
/* Tier-gated modules (course_detail.html's TieredModules — today only
   master-window-functions' last 2 modules) get their own centered row
   below the base 3-col grid instead of one more grid cell, so an
   upsell/locked card doesn't visually blend into the regular module list. */
.course-tier-row { display: flex; justify-content: center; gap: 24px; margin-top: 16px; flex-wrap: wrap; }
.course-tier-row .course-slide-card { flex: 0 1 320px; }
.course-slide-card-locked { cursor: default; opacity: 0.6; }
.course-slide-card-locked:hover { box-shadow: none; transform: none; }
/* A course still in production — listed so the catalog shows the full
   lineup, grayscale to read as "not here yet" rather than "broken", and
   (for a non-admin) not a link at all, so a click can't 404/redirect. */
.course-slide-card-unpublished { filter: grayscale(1); opacity: 0.55; }
.course-slide-card-disabled { cursor: default; }
.course-slide-card-disabled:hover { box-shadow: none; transform: none; }
.toc-card-tag.toc-card-tag-unpublished { color: var(--muted); background: var(--panel-alt); display: inline-flex; align-items: center; gap: 5px; }
.toc-card-tag.toc-card-tag-unpublished i { font-size: 9px; }
@media (max-width: 720px) {
  .course-hero-card { flex-direction: column; }
  .course-hero-thumb { flex: 0 0 auto; }
}

/* Content-delivery pages (/book, /courses, /lab): these are where a
   customer USES what they bought, not where they're sold it again — a
   short status line instead of a repeated marketing pitch. */
.content-delivery-status {
  display: inline-block;
  background: var(--panel-alt);
  color: var(--success);
  border-radius: 6px;
  padding: 8px 16px;
  font-family: 'Lato', sans-serif;
  font-size: 14px;
  font-weight: 700;
  margin: 16px 0 24px 0;
}
.content-locked-notice {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 20px 24px;
  margin: 24px 0;
  text-align: center;
}
.content-locked-notice p { margin-bottom: 12px; }
.content-locked-notice p:last-child { margin-bottom: 0; }

/* Same shape, softer tone: this isn't a wall ("you can't see this"), it's
   a non-blocking note above a full simulation of the with-access content
   (see courses_home.html/book_home.html) — accent-tinted rather than
   neutral-gray, and the content below it is never dimmed/locked-looking. */
.content-preview-notice {
  background: #eef7fa;
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 16px 24px;
  margin: 24px 0;
  text-align: center;
}
.content-preview-notice p { margin-bottom: 12px; color: var(--ink); }
.content-preview-notice p:last-child:not(:only-child) { margin-bottom: 12px; }
/* Admin variant: an amber "you're previewing a draft" tone, distinct from
   the accent-blue "subscribe to unlock" tone above — different message,
   shouldn't look like the same kind of notice. */
.content-preview-notice-admin { background: #fff8e6; border-color: #e0a94a; }
.content-preview-notice-admin p { color: #7a5b00; }

/* List of figures (book_figures.html, GET /book/figures) — a visual
   thumbnail grid, one card per figure, linking back to the chapter or
   section it actually appears in (figures render inline there; this page
   is a browsable index, not a second copy of the image at full detail —
   see figure-zoom.js on the real chapter page for that). */
.figure-list-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  margin: 24px 0;
}
@media (max-width: 960px) { .figure-list-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .figure-list-grid { grid-template-columns: 1fr; } }
.figure-list-card {
  display: block;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  text-decoration: none;
  transition: box-shadow .18s, transform .18s;
}
.figure-list-card:hover { box-shadow: 0 4px 20px rgba(55,38,73,0.12); transform: translateY(-2px); }
.figure-list-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 120px;
  padding: 14px;
  background: var(--panel-alt);
}
.figure-list-thumb img { max-width: 100%; max-height: 140px; height: auto; }
.figure-list-info { padding: 12px 16px 16px 16px; }
.figure-list-caption { font-family: 'Lato', sans-serif; font-size: 13.5px; color: var(--ink); margin-bottom: 6px; line-height: 1.4; }
.figure-list-page { font-family: 'Lato', sans-serif; font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 0; }

/* Inline figures — real pre-rendered SVGs from the book (build/epub-figs),
   embedded as-is. */
/* Capped at the same reading measure as .book-prose > p above (68ch),
   not the full white content column — on desktop that column is much
   wider than a comfortable reading line. Left-aligned (not `margin:
   auto`-centered) to match: .book-prose > p is a plain block with no
   auto margins, so it starts flush at .book-prose's own left edge — a
   centered 68ch box would center on the WIDE column's midpoint instead,
   landing well to the right of where the text column actually starts.
   text-align: center below only centers the <img> INSIDE this box, i.e.
   relative to the text column, which is what actually needed fixing. */
.sample-figure { margin: 20px 0; max-width: 68ch; text-align: center; }
.sample-figure img { max-width: 100%; height: auto; }
.sample-figure figcaption { font-family: 'Lato', sans-serif; font-size: 14px; color: var(--muted); margin-top: 10px; }
.sample-figure-note { font-family: 'Lato', sans-serif; font-size: 14px; font-style: italic; color: var(--muted); margin-top: -8px; }

/* internal/booklet's figure output — real book/course diagrams, built by
   scripts/build-figs.sh. .book-figure-pending is the honest placeholder
   shown when a chapter's been imported but its SVG hasn't been built yet
   (never a broken image). */
/* scroll-margin-top: a List of Figures link lands on #fig-N via a plain
   in-page anchor — without this the sticky .chapter-hero (desktop, see
   its own doc comment) would cover the figure's top few pixels right
   after the jump. */
.book-figure { margin: 24px 0; max-width: 68ch; text-align: center; scroll-margin-top: 90px; }
.book-figure img { max-width: 100%; height: auto; }
.book-figure figcaption { font-family: 'Lato', sans-serif; font-size: 14px; color: var(--muted); margin-top: 10px; }
.book-figure-pending {
  padding: 20px;
  border: 1px dashed var(--border);
  border-radius: 8px;
  background: var(--panel-alt);
  font-family: 'Lato', sans-serif;
  font-size: 13px;
  color: var(--muted);
}

/* figure-zoom.js: click-to-zoom lightbox for .book-figure img — figures
   render at reading-column width (68ch above) for comfortable prose,
   too small to read anything with fine text (ERDs, schema diagrams).
   One overlay shared by every figure on the page rather than one per
   image, swapping src on open. */
.figure-zoom-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: rgba(20,14,26,0.92);
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.figure-zoom-overlay.figure-zoom-open { display: flex; }
.figure-zoom-stage {
  width: 100%;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  cursor: grab;
}
.figure-zoom-img {
  max-width: 90vw;
  max-height: 82vh;
  transition: transform .04s linear;
  user-select: none;
  -webkit-user-drag: none;
  /* Several figures (TikZ diagrams, screenshots) are transparent PNGs —
     against the overlay's dark background they'd otherwise blend in
     rather than zoom in for a closer look. */
  background: #fff;
  border-radius: 4px;
}
.figure-zoom-close {
  position: absolute;
  top: 18px;
  right: 24px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.12);
  color: #fff;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}
.figure-zoom-close:hover { background: rgba(255,255,255,0.25); }
.figure-zoom-hint {
  color: rgba(255,255,255,0.6);
  font: 300 12.5px 'Lato', sans-serif;
  padding: 14px 0 20px 0;
  margin: 0;
}

/* Back-of-book Index (book_index.html, GET /book/index) — a real dt/dd
   definition list (term -> the page(s) it's discussed on), two columns
   on desktop like a print index; break-inside: avoid on each dt/dd pair
   keeps a term from splitting across the column gap. */
.index-list { font-family: 'Lato', sans-serif; column-count: 2; column-gap: 40px; margin: 12px 0 32px 0; }
.index-list dt { font-weight: 700; color: var(--heading); font-size: 14.5px; break-inside: avoid; break-after: avoid; margin-top: 8px; }
.index-list dt:first-child { margin-top: 0; }
.index-list dd { font-size: 13.5px; color: var(--muted); margin: 2px 0 0 0; break-inside: avoid; }
.index-list dd a { color: var(--accent); }
/* Subentries (Term!Sub, e.g. "People" > "Dimitri Fontaine") nest under
   their parent term — indented, lighter weight, own dd right below —
   instead of repeating "People" as a flat "People > X" row per subentry. */
.index-list dt.index-subterm {
  margin-top: 4px;
  margin-left: 14px;
  font-weight: 400;
  font-size: 13.5px;
  color: var(--ink);
}
.index-list dt.index-subterm + dd { margin-left: 14px; }
.index-letter-heading {
  font-family: 'Bookman', serif;
  color: var(--accent);
  font-size: 20px;
  margin: 28px 0 4px 0;
  break-after: avoid;
  break-inside: avoid;
}
.index-jump { display: flex; flex-wrap: wrap; gap: 4px; margin: 20px 0; font-family: 'Lato', sans-serif; font-size: 13px; }
.index-jump a { padding: 4px 8px; color: var(--heading); font-weight: 700; }
.index-jump a:hover { color: var(--accent); }

/* Concept list on intro-3 */
.sample-concept-list { font-family: 'Lato', sans-serif; font-size: 17px; }
.sample-concept-list li { margin-bottom: 10px; }

/* ─── Per-query "try this query" toggle — top-right corner of each
       runnable code block's header, one switch per block, running
       server-side against the shared lab database
       (public/js/sample-interactive.js, internal/handlers/bookquery.go).
       Works on any device. ──────────────────────────────────────────── */
.code-block-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 12px 8px 16px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.code-block-header .code-block-label { padding: 0; border-bottom: none; }
.code-toggle {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-shrink: 0;
}
.code-toggle-label {
  font-family: 'Lato', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--faint-on-dark);
  white-space: nowrap;
}
.code-toggle-switch {
  position: relative;
  flex-shrink: 0;
  width: 34px; height: 19px;
  border-radius: 10px;
  background: var(--faint-on-dark);
  border: none;
  cursor: pointer;
  transition: background .15s linear;
  padding: 0;
}
.code-toggle-switch::before {
  content: '';
  position: absolute;
  top: 2.5px; left: 2.5px;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: #fff;
  transition: transform .15s linear;
}
.code-toggle-switch[aria-pressed="true"] { background: var(--accent); }
.code-toggle-switch[aria-pressed="true"]::before { transform: translateX(15px); }
.code-toggle-switch:disabled { opacity: .6; cursor: progress; }

/* ─── Code blocks: static syntax highlighting + editable/runnable overlay
       (ported from TheArtOfPostgreSQL/src/query-ui's tokenizer/overlay) ── */
/* Fit the block to its content's natural width (longest code line, or the
   header/actions row if that needs more) rather than always stretching to
   the full column — a short query doesn't need a mostly-empty wide box.
   Wide results/plans still get the full column and scroll internally
   (.code-result's own overflow-x: auto), since max-width caps growth here. */
/* Same reading measure as .book-prose > p/.book-figure (68ch) — was
   max-width: 100% of .book-prose's own (much wider) column, so a code
   block could stretch to the full page width instead of the text
   column next to it. Still width: fit-content below that cap, so a
   short query stays its own natural width instead of stretching to
   fill 68ch. */
.code-block { margin: 20px 0; border-radius: 8px; overflow: hidden; background: var(--code-bg); width: fit-content; max-width: min(100%, 68ch); }
.code-block-label {
  font-family: 'Lato', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--faint-on-dark);
  padding: 8px 16px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.code-block pre.code-static {
  margin: 0;
  padding: 14px 16px;
  background: transparent;
  border: none;
  border-radius: 0;
  color: #f8f8f2;
  font-size: 13.5px;
  overflow-x: auto;
}

/* Editable overlay: identical technique to the lab's query-ui — an
   invisible-text textarea sits over a highlighted <pre>, sharing exact
   padding/font metrics, so the caret/selection are real but the glyphs the
   reader sees come from the colored <pre> underneath. */
.code-editor-wrapper { position: relative; min-height: 60px; }
.code-editor-highlight, .code-editor-input {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 14px 16px;
  border: none;
  background: transparent;
  font-family: 'JetBrains Mono', 'Fira Code', monospace;
  font-size: 13.5px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow: auto;
}
.code-editor-highlight { pointer-events: none; color: #f8f8f2; }
.code-editor-input { z-index: 1; width: 100%; height: 100%; color: transparent; caret-color: #f8f8f2; resize: vertical; }
.code-editor-input::selection { background: rgba(96,178,211,0.35); }
.code-block-actions { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-top: 1px solid rgba(255,255,255,0.08); }
.code-block-actions .button { min-height: 32px; padding: 6px 18px; font-size: 12px; }
.code-block-actions .button:not(.button_purple) { border-color: var(--faint-on-dark); color: var(--faint-on-dark); }
.code-block-actions .button:not(.button_purple):hover { background: var(--faint-on-dark); color: var(--code-bg); }
.code-block-status { font-family: 'Lato', sans-serif; font-size: 12px; color: var(--faint-on-dark); }

/* Query results: monospace (matches the lab's own results pane) so columns
   of numbers/dates line up; numeric cells right-align for the same reason. */
.code-result { background: #fff; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 0 16px 16px 16px; }
.code-result table { border-collapse: collapse; width: 100%; font-size: 13px; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
.code-result th, .code-result td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.code-result th { background: var(--panel-alt); }
.code-result td.num { text-align: right; }
.code-result td.td-json { white-space: normal; max-width: 420px; vertical-align: top; }
.code-result .cell-null { color: var(--muted); font-style: italic; }
.code-result .json-cell { max-height: 200px; font-size: 12px; }
/* .code-result's own background is white (not code-bg) — same contrast
   reasoning as .flash.error above: var(--danger) reads fine on a dark
   surface, not here. */
.code-result.code-result-error { color: #a4454f; font-family: 'JetBrains Mono', monospace; font-size: 13px; }

/* Persistent Lab console (public/js/lab-console.js) — a real free-form SQL
   textarea against the student's own server-side sandbox, not a code
   block with a pre-set query. Result table reuses .code-result as-is. */
.lab-console { margin-top: 24px; }
.lab-console-input {
  width: 100%;
  box-sizing: border-box;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--code-bg);
  color: #fff;
  font-family: 'JetBrains Mono', 'Fira Code', monospace;
  font-size: 14px;
  resize: vertical;
}
.lab-console-actions { display: flex; gap: 10px; margin: 12px 0; }
.lab-console-result:empty { display: none; }
.lab-console-status { font-family: 'Lato', sans-serif; font-size: 14px; color: var(--muted); margin: 0 16px 16px 16px; }
.lab-console-error {
  margin: 0 16px 16px 16px;
  padding: 12px 16px;
  color: #a4454f;
  background: #fdf1f2;
  border-radius: 6px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  white-space: pre-wrap;
}

/* EXPLAIN pane: Text / Diagram / JSON tabs, ported from query-ui */
.explain-tabs { display: flex; gap: 4px; margin-bottom: 10px; border-bottom: 1px solid var(--border); }
.explain-tab {
  padding: 6px 14px; border: none; background: transparent; cursor: pointer;
  font-family: 'Lato', sans-serif; font-size: 13px; color: var(--muted);
  border-bottom: 2px solid transparent;
}
.explain-tab:hover { color: var(--ink); }
.explain-tab.active { color: var(--heading); border-bottom-color: var(--accent); font-weight: 700; }
.explain-tab-content pre.explain-text {
  background: var(--code-bg); color: #f8f8f2; font-size: 12.5px;
  font-family: 'JetBrains Mono', monospace; padding: 12px; border-radius: 6px; overflow-x: auto;
}
.json-cell {
  margin: 0; max-height: 60vh; overflow: auto; background: var(--code-bg); color: #f8f8f2;
  padding: 10px; border-radius: 6px; font-size: 12.5px; font-family: 'JetBrains Mono', monospace; white-space: pre;
}
.explain-diagram { background: #2c2f36; border-radius: 6px; padding: 10px; overflow: auto; }
.explain-diagram-header { padding: 0 0 10px 0; color: var(--muted); font-family: 'Lato', sans-serif; font-size: 12px; }

/* Token colors match the marketing site's own code highlighting exactly —
   its Chroma/Monokai theme (static/syntax.css: .chroma .k/.s/.c/.m/.nt),
   not an invented palette, so a code block here and one on theartofpostgresql.com
   read as the same "kind" of syntax highlighting. */
.tok-keyword  { color: #66d9ef; }
.tok-string   { color: #e6db74; }
.tok-comment  { color: #75715e; font-style: italic; }
.tok-number   { color: #ae81ff; }
.tok-key      { color: #f92672; }

.sample-mobile-notice {
  background: var(--code-bg);
  color: var(--muted-on-dark);
  border-radius: 8px;
  padding: 18px 22px;
  margin: 28px 0;
  font-family: 'Lato', sans-serif;
  font-size: 14px;
}

/* ─── "Buy the book" CTA — a full-bleed section sitting just before the
       footer, same treatment (and same real precedent) as the marketing
       site's own .index_final_cta (style.css) — the "final CTA before the
       footer" section on its homepage: dark ink background, white Bookman
       heading, light body text, accent-blue primary button. The earlier
       accent-blue full-section background here had no such precedent
       anywhere on the real site (accent blue is a button/badge color
       there, never a full-section fill) — this replaces it with a
       pattern that's actually used, not just an on-palette color used in
       a novel way. */
.sample-signup {
  /* Same slide-bg surface as .app-shell-sidebar/.chapter-hero, with the
     same background-attachment: fixed so the image lines up continuously
     across this section's boundary with the sidebar right next to it
     (see .chapter-hero's own doc comment on why: without "fixed",
     background-position/-size resolve against this element's own box —
     full-bleed and short — instead of the viewport, a different
     crop/scale than the sidebar's tall narrow one). */
  background: var(--header-bg) url('/images/hero-bg.jpg') center top no-repeat;
  background-size: cover;
  background-attachment: fixed;
  padding: 56px 20px;
  text-align: center;
}
.sample-signup .wrapper { max-width: 640px; }
.sample-signup-icon { font-size: 2rem; margin-bottom: 8px; }
.sample-signup-title { font: 400 28px 'Bookman'; color: #fff; margin-bottom: 10px; }
.sample-signup-tagline { font-family: 'Lato', sans-serif; font-size: 16px; color: var(--muted-on-dark); margin-bottom: 28px; }
.sample-signup-cta-row { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; }
.sample-signup-cta-row .button.button_purple { background: var(--accent); border-color: var(--accent); }
.sample-signup-cta-row .button.button_purple:hover { background: var(--heading); border-color: var(--accent); }
.sample-signup-cta-row .button.button_transparent,
.sample-signup-cta-row .button.button_transparent:visited { border-color: rgba(255,255,255,0.5); color: rgba(255,255,255,0.9); }
.sample-signup-cta-row .button.button_transparent:hover { background: rgba(255,255,255,0.12); border-color: #fff; color: #fff; }
.sample-signup-note { color: var(--muted-on-dark); font-family: 'Lato', sans-serif; font-size: 13px; margin: 22px 0 0 0; }
.sample-signup-note a { color: #fff; text-decoration: underline; font-weight: 700; }

/* Admin — nav chrome (app-shell-sidebar/app-shell-nav, .chapter-hero) is
   all reused as-is from the student app (see "admin_header"/"admin_hero"
   in partials.html); only the page content area below needs its own
   rules. */
.admin-hero h1 { color: #fff; font: 400 28px 'Bookman'; margin: 0; }
.admin-content { padding: 40px; }
.admin-content h3, .admin-content h4 { margin-top: 1.5em; }
@media (max-width: 720px) {
  .admin-content { padding: 24px 16px; }
}
.stat-tiles { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 30px; }
.stat-tile { flex: 1 1 160px; background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 20px; }
.stat-tile .value { font: 400 32px 'Bookman'; color: var(--heading); }
.stat-tile .label { font: 700 11px 'Lato'; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); }
table.admin-table { width: 100%; border-collapse: collapse; background: #fff; }
table.admin-table th, table.admin-table td { padding: 10px 14px; border-bottom: 1px solid var(--border); text-align: left; font-size: 14px; }
table.admin-table th { font: 700 11px 'Lato'; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); background: var(--panel-alt); }
table.admin-table td a { color: var(--body-text); text-decoration: none; }
table.admin-table td a:hover { color: var(--heading); text-decoration: underline; }
.admin-table-nowrap { white-space: nowrap; }
/* ThriveCart transaction dates ("2026-08-01 14:32:07") wrap onto two lines
   at the default column width — give that column enough room to stay on
   one line. */
table.admin-table-tc-transactions th:first-child,
table.admin-table-tc-transactions td:first-child { width: 1%; white-space: nowrap; }
/* Courses Activity renders one <table> per course; fixed widths keep the
   Module/Sample/Completed by columns aligned vertically down the page even
   though each table is an independent instance. */
table.admin-table-courses-activity { table-layout: fixed; margin-bottom: 30px; }
table.admin-table-courses-activity th:nth-child(1), table.admin-table-courses-activity td:nth-child(1) { width: 60%; }
table.admin-table-courses-activity th:nth-child(2), table.admin-table-courses-activity td:nth-child(2) { width: 15%; }
table.admin-table-courses-activity th:nth-child(3), table.admin-table-courses-activity td:nth-child(3) { width: 25%; }
/* Book Activity renders one <table> per part, same reasoning as Courses
   Activity above — #/Chapter/Sample/Completed by kept aligned down the
   page across every part's own table instance. */
table.admin-table-book-activity { table-layout: fixed; margin-bottom: 30px; }
table.admin-table-book-activity th:nth-child(1), table.admin-table-book-activity td:nth-child(1) { width: 6%; }
table.admin-table-book-activity th:nth-child(2), table.admin-table-book-activity td:nth-child(2) { width: 59%; }
table.admin-table-book-activity th:nth-child(3), table.admin-table-book-activity td:nth-child(3) { width: 15%; }
/* Static assets: Size/Last built are short, fixed-format values ("2.9 MB",
   "2026-08-02 15:13") that otherwise wrap onto two lines at the default
   column width. */
table.admin-asset-table th:last-child, table.admin-asset-table td:last-child,
table.admin-asset-table th:nth-last-child(2), table.admin-asset-table td:nth-last-child(2) {
  white-space: nowrap;
}
table.admin-asset-table td code { font-size: 12px; word-break: break-all; }
/* Per-module asset tables: one <table> per course module, so fixed
   widths (same reasoning as admin-table-courses-activity/book-activity
   above) keep Asset kind/Path/Size/Last built aligned vertically from one
   module's table to the next. */
table.admin-module-asset-table { table-layout: fixed; margin-bottom: 26px; }
table.admin-module-asset-table caption {
  text-align: left; font: 700 13px 'Lato'; color: var(--heading); padding: 8px 0; caption-side: top;
}
table.admin-module-asset-table th:nth-child(1), table.admin-module-asset-table td:nth-child(1) { width: 14%; }
table.admin-module-asset-table th:nth-child(2), table.admin-module-asset-table td:nth-child(2) { width: 54%; }
table.admin-module-asset-table th:nth-child(3), table.admin-module-asset-table td:nth-child(3) { width: 14%; }
table.admin-module-asset-table th:nth-child(4), table.admin-module-asset-table td:nth-child(4) { width: 18%; }
table.admin-table-book-activity th:nth-child(4), table.admin-table-book-activity td:nth-child(4) { width: 20%; }
.role-tag { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 700; text-transform: uppercase; }
.role-tag.admin { background: #2c1739; color: #fff; }
.role-tag.student { background: var(--panel-alt); color: var(--body-text); }
.role-tag.queued { background: var(--panel-alt); color: var(--body-text); }
.role-tag.running { background: #fff3cd; color: #7a5b00; }
.role-tag.succeeded { background: #d4edda; color: #1e5e2b; }
.role-tag.failed { background: #f8d7da; color: #7a1f27; }

/* Users table: Created/Last login get a fixed no-wrap width (their
   content is a short fixed-format date/time, so wrapping is always just
   an artifact of the column being squeezed by the wider Products cell,
   never a real overflow); Products becomes a row of tag pills instead of
   a raw "book:full, courses:core" string. */
.admin-users-col-created, .admin-users-col-lastlogin { white-space: nowrap; width: 1%; }
.product-tag {
  display: inline-flex;
  align-items: center;
  margin: 2px 4px 2px 0;
  border-radius: 10px;
  overflow: hidden;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
}
.product-tag-product { padding: 2px 8px; background: var(--heading); color: #fff; }
.product-tag-tier { padding: 2px 8px; background: var(--panel-alt); color: var(--body-text); }

/* Book/Courses icon tags: an icon alone means "has this product" (Sample
   is every account's free baseline — see buildProductInfo's own doc
   comment for why it's never shown, since it's true of every row and
   would just be noise); an extra label only appears for a courses tier
   above Core, since a bare icon already means "has Core". Kept visually
   distinct from .product-tag (workshop etc.) since these two are the
   product lineup a reader scans first, row after row. */
.product-icon-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin: 2px 4px 2px 0;
  padding: 3px 8px;
  border-radius: 10px;
  background: #7a5f96;
  color: #fff;
  font-size: 13px;
}
.product-icon-tag-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; }

.admin-note { color: var(--muted); font-size: 13px; margin-top: -18px; margin-bottom: 24px; }
.admin-success { color: #1f6d3a; font-size: 13px; margin-bottom: 24px; }
.admin-actions { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 30px; }
.admin-action-card { flex: 1 1 320px; background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 20px; }
.admin-action-title { font: 700 15px 'Lato'; color: var(--heading); margin-bottom: 8px; }
.admin-action-title i { margin-right: 8px; color: var(--muted); }
.admin-action-desc { font-size: 13px; color: var(--muted); margin-bottom: 16px; }
.admin-planned-note { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--muted); font-size: 12px; }
.admin-planned-list { font-size: 14px; color: var(--body-text); line-height: 1.7; margin-bottom: 30px; }
/* One joined bar, not an input with a separate button next to it: an
   explicit shared height with box-sizing: border-box on both (rather
   than relying on padding+line-height math to happen to add up the same
   way — a native <button>'s own default sizing/appearance in some
   browsers doesn't add up the same as an <input>'s, even with identical
   padding/font declarations), no gap, and the border-radius only on the
   two outer corners so the seam between them reads as one control. Both
   also center their text via flex, rather than trusting line-height
   alone to vertically center it inside the shared height. */
/* display: inline-flex rather than a block stretched to max-width: the
   bar should hug the width of its own contents (placeholder text + button
   label), not stretch to fill the wrapper and leave a mostly-empty input
   behind a short label. */
.admin-job-progress {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  font-size: 13px;
  color: var(--muted);
}
.admin-job-progress[hidden] { display: none; }
.admin-job-progress i { color: var(--accent); }

.admin-updates-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 640px;
  margin-bottom: 28px;
  padding: 18px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.admin-updates-form label { display: flex; flex-direction: column; gap: 4px; font: 700 12px 'Lato'; color: var(--muted); flex: 1; }
.admin-updates-form input, .admin-updates-form select, .admin-updates-form textarea {
  font: 400 14px 'Lato';
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--ink);
  background: var(--panel-alt);
}
.admin-updates-form-row { display: flex; gap: 12px; }

.admin-lookup-form { display: inline-flex; margin-bottom: 8px; }
.admin-lookup-form input {
  width: 260px;
  flex: none;
  height: 44px;
  box-sizing: border-box;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-right: none;
  border-radius: 6px 0 0 6px;
  font: 400 14px 'Lato';
}
/* A plain rectangular box button, not the marketing site's rounded pill
   (.button) — this is a utility action inside an admin tool, not a CTA. */
.admin-lookup-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  box-sizing: border-box;
  margin: 0;
  padding: 0 18px;
  border: 1px solid var(--heading);
  border-radius: 0 6px 6px 0;
  background: var(--heading);
  color: #fff;
  font: 700 13px 'Lato';
  letter-spacing: 0.3px;
  cursor: pointer;
  white-space: nowrap;
  appearance: none;
  -webkit-appearance: none;
}
.admin-lookup-btn:hover { background: #000; border-color: #000; }
.admin-lookup-error { color: #7a1f27; font-size: 13px; margin-bottom: 24px; }
/* Filter (left) and Sync (right) share one row instead of stacking as two
   separate full-width blocks — they're both toolbar actions for the same
   table, not sequential steps. */
.admin-users-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.admin-users-filter-form { display: flex; align-items: center; gap: 12px; margin-bottom: 0; }
.admin-users-filter-form select {
  height: 40px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: #fff;
  font: 400 14px 'Lato';
  color: var(--body-text);
}
.admin-users-filter-clear { font-size: 13px; color: var(--muted); text-decoration: underline; }
.admin-users-dev-toggle { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); white-space: nowrap; }
.admin-users-toolbar h3 { margin: 0; }
.admin-table-row-inactive { opacity: 0.5; }
.admin-popularity-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 10px; }
.admin-product-activity { margin-bottom: 40px; }
.admin-product-activity h2 { margin-bottom: 12px; }

/* Build-status buttons styled like a terminal command rather than the
   marketing site's rounded pill buttons — this page is a technical/ops
   surface (rebuild triggers, logs), not a conversion page. */
.admin-build-btn {
  font: 400 13px/1 'Menlo', 'Consolas', monospace;
  background: #1c1c22;
  color: #d7d7db;
  border: 1px solid #333;
  border-radius: 4px;
  padding: 8px 14px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.admin-build-btn:hover { background: #26262e; border-color: #4a4a55; }
.admin-build-btn-prompt { color: #6fce6f; margin-right: 6px; }
.admin-build-btn-all { margin-top: 4px; }
.admin-impersonate-form { text-align: center; }
/* Plain rectangle, light color — deliberately NOT the terminal/build-
   status treatment (.admin-build-btn) or the marketing pill (.button):
   this is a routine action button, not an ops/technical command. */
.admin-btn-plain {
  display: inline-block;
  padding: 10px 18px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #fff;
  color: var(--body-text);
  font: 700 13px 'Lato';
  letter-spacing: 0.3px;
  cursor: pointer;
  text-decoration: none;
}
.admin-btn-plain:hover { background: var(--panel-alt); }
.admin-publish-actions { display: flex; align-items: center; gap: 10px; }
.admin-publish-toggle-form { margin: 0; }

/* Footer — same values as the marketing site's own .footer (taop.xyz/
   themes/taop/static/css/style.css), simplified to two columns (FAQ left,
   What's New right — see the "footer" template's own doc comment for why
   about-the-author was dropped) instead of that site's three. Class
   names below (faq_block, whatsnew_block, copyright) are kept identical
   to that stylesheet on purpose for the parts that are still a direct
   port. */
.app-footer {
  /* Same slide-bg surface as .app-shell-sidebar/.chapter-hero, for visual
     continuity between the sidebar the reader has been looking at and
     the footer they land on at the end of the page. */
  background: var(--header-bg) url('/images/hero-bg.jpg') center top no-repeat;
  background-size: cover;
  background-attachment: fixed;
  color: #fff;
  padding: 75px 0 0 0;
}
.app-footer a, .app-footer a:visited { color: inherit; }
.app-footer .wrapper { font-size: 16px; line-height: 26px; font-weight: 300; }
.footer-two-col { display: flex; flex-wrap: wrap; gap: 60px; }
.app-footer h6 {
  font-size: 16px; letter-spacing: 3px; padding-bottom: 35px; color: #fff;
  text-transform: uppercase; text-align: left;
}
.app-footer h6::before { content: ''; display: block; width: 125px; height: 2px; background: #fff; margin-bottom: 10px; }

.faq_block { flex: 2 1 480px; text-align: left; padding: 0 0 40px 0; box-sizing: border-box; font-family: 'Lato', sans-serif; }
.whatsnew_block { flex: 1 1 260px; text-align: left; padding: 0 0 40px 0; font-family: 'Lato', sans-serif; }
.whatsnew_block ul { list-style: none; padding: 0; margin: 0; border-top: 1px solid #464150; }
.whatsnew_block ul li { border-bottom: 1px solid #464150; padding: 6px 0; font-size: 15px; font-weight: 300; line-height: 20px; }
.whatsnew_block ul li a, .whatsnew_block ul li a:visited { color: #d1d0d3; text-decoration: none; }
.whatsnew_block ul li a:hover { color: #fff; }

.faq_block dl { border-bottom: 1px solid #464150; margin: 0; }
.faq_block dl dt {
  cursor: pointer; padding: 5px 20px 5px 0; position: relative; border-top: 1px solid #464150;
  font-size: 16px; font-weight: 400;
}
.faq_block dl dt::after {
  content: ''; position: absolute; top: 50%; right: 2px; margin: -1px 0 0 -5px;
  width: 10px; height: 2px; background: #fff;
}
.faq_block dl dt::before {
  content: ''; position: absolute; top: 50%; right: 6px; margin: -5px 0 0 -1px;
  width: 2px; height: 10px; background: #fff; transition: transform 0.25s ease-out;
}
.faq_block dl dt.active::before { transform: rotate(90deg); }
.faq_block dl dd { display: none; padding: 0 0 10px 0; font-size: 15px; line-height: 20px; font-weight: 300; color: #d1d0d3; margin: 0; }
.faq_block dl dt.active + dd { display: block; }
.faq_block dl dd a, .faq_block dl dd a:visited { color: #fff; font-weight: 500; }
.faq_block dl dd a:hover { color: #d1d0d3; }

.copyright {
  color: #fff; text-align: center; font-weight: 100; font-size: 12px;
  letter-spacing: 0.5px; padding: 20px 0; margin-top: 40px;
  border-top: 1px solid rgba(255,255,255,0.15);
}

@media (max-width: 767px) {
  .app-footer { padding: 50px 10px 10px 10px; }
  .faq_block dl dt { font-size: 15px; line-height: 20px; }
  .faq_block dl dd { padding: 5px 10px 15px 10px; }
}
@media (max-width: 600px) {
  .footer-two-col { display: block; }
}

@media (max-width: 900px) {
  .index-list { column-count: 1; }
}
/* Mobile header — same breakpoint and behavior as the marketing site's
   own (style.css, "@media only screen and (max-width: 767px)"): nav
   items collapse to icon-only (text hidden) and the purchase CTA moves
   into a fixed bottom bar instead of the top-right corner. */
@media (max-width: 767px) {
  /* Dev bar is a local-dev-only tool (config.AuthDevBypass) — never seen
     on a real mobile visit anyway, and just eats vertical space when
     testing the responsive layout in a narrow browser viewport. Hide it
     and undo the space it would otherwise reserve above the header. */
  .dev-toggle-bar { display: none; }
  body.has-dev-bar .header { top: 0; }
  body.has-dev-bar { padding-top: 71px; }
  .header nav ul { flex-wrap: nowrap; }
  .header nav ul li { padding: 20px 8px; white-space: nowrap; }
  .header nav ul li.divider { margin: 0 4px; }
  .header nav ul li a i { display: block; font-size: 18px; text-align: center; }
  .header nav ul li a .nav-text { display: none; }
  .header .sticky-cta-wrapper {
    position: fixed;
    top: auto; bottom: 0; left: 0; right: 0;
    transform: none;
    z-index: 999;
    background: var(--header-bg);
    padding: 10px 20px;
    box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
  }
  .header .sticky-cta { display: block; border-radius: 0; padding: 15px; font-size: 14px; }
  body { padding-bottom: 56px; }
  .admin-popularity-cols { grid-template-columns: 1fr; }
}
