:root {
  --paper: #FFFFFF;
  --paper-raised: #FFFFFF;
  --mist: #F5F1E8;
  --ink: #000000;
  --ink-soft: #6B655A;
  --line: #DAD4C4;
  --accent: #EA5B2F;
  --accent-deep: #B8431E;
  --accent-soft: #FBD9C7;
  --pill-radius: 999px;
  --radius: 3px;
  /* Neutral backdrop visible around the edges of the framed content card -
     deliberately a shade darker/greyer than --paper so the card reads as
     a distinct surface sitting on top of it, not just more page. */
  --page-backdrop: #FFFFFF;
  /* Helvetica isn't distributable as a web font (Monotype-licensed) - this
     stack renders as real Helvetica Neue on any Mac/iPhone since it ships
     with the OS, and falls back to Arial (metrically near-identical)
     everywhere else. */
  --font-sans: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* Georgia is a licensed-for-web-use system serif on every major OS, so
     no @font-face/webfont import is needed - same reasoning as the
     Helvetica stack above. Used only on the landing page headline. */
  --font-serif: Georgia, "Times New Roman", Times, serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--page-backdrop);
  color: var(--ink);
  font-family: var(--font-sans);
  line-height: 1.5;
}

/* ---------- Page frame ---------- */
/* The actual site content sits in this fixed-max-width card, centered on
   the neutral backdrop above. This is the fix for the hero image "zoom"
   problem that kept moving as a target: previously the hero photo spanned
   the entire browser viewport with no cap, so every resize forced it to
   recalculate its crop from scratch. Now the card stops growing past
   --page-frame-width and any extra space just becomes more visible
   backdrop around it - the hero image's framing never has to change. */
.page-frame {
  --page-frame-width: 1240px;
  position: relative;
  max-width: var(--page-frame-width);
  margin: 0 auto;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 22px;
  overflow: hidden;
}

@media (max-width: 640px) {
  .page-frame {
    border-left: none;
    border-right: none;
    border-radius: 0;
  }
}

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

/* ---------- Site nav ---------- */

.site-nav {
  position: relative;
  z-index: 1;
  background: #FFFFFF;
  border-bottom: 1px solid var(--line);
}

.site-nav-inner {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0.85rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 2.25rem;
}

.site-logo {
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--ink);
  text-decoration: none;
  letter-spacing: -0.005em;
  white-space: nowrap;
}

.site-nav-links {
  display: flex;
  gap: 1.6rem;
  flex: 1;
}

.site-nav-links a {
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--ink-soft);
  text-decoration: none;
}

.site-nav-links a:hover { color: var(--ink); }

.site-login {
  padding: 0.45rem 1.05rem;
  font-size: 0.78rem;
  text-transform: none;
  letter-spacing: 0;
}

/* ---------- Page-load fade-in (top down) ---------- */
/* Runs once on load, not on scroll - the nav header animates first since
   it's the literal top of the page, then each major section cascades in
   after it, with the sky overlay (checker + report pages) coming in last
   of all - a background element revealing behind everything once the
   foreground content is already in place, rather than announcing itself
   first. This intentionally does NOT apply to the loading page's own
   sky background (#status) - that one is baked directly into #status's
   background-image and appears instantly with the rest of the loading
   panel when it slides in, not part of this fade cascade at all. Uses
   forwards fill-mode since this is a one-time reveal, not a loop like
   the report-loading animation.

   Each direct child of .page-fade-in cascades in turn including <main>
   (5th child - previously had no stagger of its own and defaulted to the
   same 0s delay as the first section, which broke the cascade), the
   footer closes out the checker page's own content, and the sky overlay
   comes in last of all, after even the footer. */

.site-nav {
  opacity: 0;
  transform: translateY(-16px);
  animation: page-fade-down 0.6s ease-out forwards;
}

.page-fade-in {
  position: relative;
  z-index: 1;
}

.page-fade-in > * {
  opacity: 0;
  transform: translateY(-16px);
  animation: page-fade-down 0.6s ease-out forwards;
}

.page-fade-in > *:nth-child(1) { animation-delay: 0.15s; }
.page-fade-in > *:nth-child(2) { animation-delay: 0.3s; }
.page-fade-in > *:nth-child(3) { animation-delay: 0.45s; }
.page-fade-in > *:nth-child(4) { animation-delay: 0.6s; }
.page-fade-in > *:nth-child(5) { animation-delay: 0.75s; }

body.page-checker footer {
  opacity: 0;
  transform: translateY(-16px);
  animation: page-fade-down 0.6s ease-out forwards;
  animation-delay: 0.9s;
}

/* Last in the cascade on every page that uses it (checker, report) -
   deliberately excludes the loading page's #status background, which
   isn't part of this system at all (see note above). */
.page-sky-overlay {
  opacity: 0;
  transform: translateY(-16px);
  animation: page-fade-down 0.6s ease-out forwards;
  animation-delay: 1.05s;
}

@keyframes page-fade-down {
  from { opacity: 0; transform: translateY(-16px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---------- Hero ---------- */

/* The sky photo itself lives on .page-sky-overlay (see below), positioned
   behind both .site-nav and .hero-photo so one continuous image spans
   both - .hero-photo just needs to get out of the way and let it show
   through, rather than owning the image itself. */
.hero-photo {
  position: relative;
  width: 100%;
  min-height: 320px;
  background-color: transparent;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}

/* Sits behind .site-nav + .hero-photo on the checker page only (see the
   body.page-checker scoping above and templates/base.html's
   {% block frame_overlay %}). Height is tuned to the source image's own
   aspect ratio at the page-frame's ~1240px width, so the image's built-in
   alpha fade (opaque at top, transparent by the bottom of the file) lands
   right around where the hero content ends - no hard-coded CSS fade
   needed on top of it. Clipped to the page-frame's rounded top corners
   via the page-frame's own overflow:hidden. */
.page-sky-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 600px;
  background-image: url('/static/img/home-hero-sky.png');
  background-repeat: no-repeat;
  background-position: top center;
  background-size: cover;
  pointer-events: none;
  z-index: 0;
}

@media (max-width: 720px) {
  .page-sky-overlay { height: 420px; }
}

.hero-photo-text {
  max-width: 480px;
  text-align: center;
  margin: 0 auto;
  padding-top: 2.5rem;
}

.hero-photo h1 {
  font-weight: 700;
  font-size: 2.4rem;
  margin: 0 0 0.5rem;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.hero-sub {
  margin: 0;
  color: var(--ink);
  opacity: 0.75;
  font-size: 1rem;
}

.hero-bookmark-card {
  max-width: 620px;
  margin: -3.5rem auto 0;
  position: relative;
  background: var(--paper-raised);
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  padding: 1.5rem 1.75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  z-index: 2;
}

.hero-bookmark-card .hint {
  margin: 0;
  max-width: 30ch;
}

/* Wrapper for the two elements moved out of .intake (bookmarks-bar tip +
   manual copy-row) so they sit directly under .hero-bookmark-card instead
   of down in the main content column - matches that card's 620px width/
   centering since .intake's own max-width (880px, see main{}) doesn't
   apply out here. */
.bookmark-help {
  max-width: 620px;
  margin: 1.5rem auto 0;
  padding: 0 1.5rem;
}

@media (max-width: 720px) {
  .hero-photo { justify-content: center; }
  .hero-photo-text { text-align: center; margin: 0 1.5rem; }
  .hero-bookmark-card { flex-direction: column; align-items: stretch; margin-left: 1.5rem; margin-right: 1.5rem; }
}

/* ---------- Simple page header (About/Contact/Donate) ---------- */

.page-header {
  max-width: 680px;
  margin: 0 auto;
  padding: 2.5rem 1.5rem 0.5rem;
}

.page-header h1 {
  font-weight: 800;
  font-size: 2rem;
  margin: 0 0 0.4rem;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.page-subtext {
  margin: 0;
  color: var(--ink-soft);
  font-size: 1rem;
}

.stamp {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 2px solid currentColor;
  border-radius: var(--pill-radius);
  padding: 0.5rem 0.9rem;
  transform: rotate(-6deg);
  white-space: nowrap;
}

/* Severity is conveyed by weight, not hue, to stay within the cream/white/
   black/orange palette: near-black for the most serious flags, orange for
   "worth noting," light mist for clean. */
.stamp-likely { color: var(--paper); background: var(--ink); border-color: var(--ink); }
.stamp-possible { color: var(--ink); background: var(--accent-soft); border-color: var(--accent); }
.stamp-clean { color: var(--ink-soft); background: var(--mist); border-color: var(--line); }

/* ---------- Layout ---------- */

main {
  position: relative;
  z-index: 1;
  max-width: 880px;
  margin: 0 auto;
  padding: 2.5rem 1.5rem 4rem;
}

/* ---------- Intake ---------- */

.premium-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.8rem;
  background: var(--accent-soft);
  border: 1.5px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.9rem 1.2rem;
  margin-bottom: 1.8rem;
  font-size: 0.85rem;
}

#premiumStatus {
  color: var(--ink);
}

#premiumStatus.premium-active {
  color: var(--accent);
  font-weight: 600;
}

#premiumUnlockRow input {
  padding: 0.5rem 0.7rem;
  font-size: 0.85rem;
}

#premiumUnlockRow .btn {
  padding: 0.5rem 1rem;
  font-size: 0.75rem;
}

.tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.25rem;
  border-bottom: 1px solid var(--line);
}

.tab {
  font-family: var(--font-sans);
  font-size: 0.82rem;
  letter-spacing: 0.03em;
  background: none;
  border: none;
  padding: 0.7rem 0.2rem;
  margin-right: 1.5rem;
  color: var(--ink-soft);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transform: translateY(1px);
}

.tab.active {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.tab-tag {
  font-size: 0.66rem;
  color: var(--accent);
  text-transform: none;
  letter-spacing: 0;
  margin-left: 0.3rem;
  opacity: 0.85;
}

.tab.active .tab-tag { color: var(--accent); }

.tab-panel { display: none; }
.tab-panel.active { display: block; }

label {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  margin-bottom: 0.5rem;
}

.row { display: flex; gap: 0.6rem; }

input[type="text"], input[type="email"], textarea {
  flex: 1;
  font-family: var(--font-sans);
  font-size: 0.95rem;
  padding: 0.75rem 0.9rem;
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper-raised);
  color: var(--ink);
}

textarea { width: 100%; resize: vertical; }

input[type="text"]:focus, input[type="email"]:focus, textarea:focus, .btn:focus-visible, .tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.contact-section {
  max-width: 560px;
  margin: 0 auto;
  padding: 1.5rem 1.5rem 3rem;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.contact-form label:not(:first-child) {
  margin-top: 0.6rem;
}

.contact-form .btn {
  align-self: flex-start;
  margin-top: 0.4rem;
}

.btn {
  font-family: var(--font-sans);
  font-size: 0.85rem;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  padding: 0.65rem 1.35rem;
  border-radius: var(--pill-radius);
  border: 1.5px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  cursor: pointer;
}

.btn:hover { background: var(--accent); border-color: var(--accent); }
.btn:active { background: var(--accent-deep); border-color: var(--accent-deep); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.hint {
  font-size: 0.82rem;
  color: var(--ink-soft);
  margin: 0.6rem 0 0;
}

.bookmarklet-box {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.2rem;
  background: var(--accent-soft);
  border: 1.5px dashed var(--accent);
  border-radius: var(--radius);
  margin-bottom: 0.75rem;
}

.bookmarklet-btn {
  cursor: grab;
  user-select: none;
}

.bookmarklet-btn:active { cursor: grabbing; }

.bookmarklet-copy-row {
  margin-bottom: 1rem;
}

.bookmarklet-copy-row .row {
  margin-top: 0.5rem;
}

#bookmarkletUrl {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  color: var(--ink-soft);
  background: var(--paper-raised);
}

.dropzone {
  border: 1.5px dashed var(--line);
  border-radius: var(--radius);
  padding: 1.8rem;
  text-align: center;
  background: var(--paper-raised);
  margin-bottom: 1rem;
}

.dropzone.drag { border-color: var(--accent); background: var(--accent-soft); }

.link-like {
  color: var(--accent);
  text-decoration: underline;
  cursor: pointer;
}

/* ---------- Status (full-page overlay) ---------- */

.status {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  background-color: var(--paper);
  background-image: url('/static/img/home-hero-sky.png');
  background-repeat: no-repeat;
  background-position: top center;
  background-size: cover;
  transform: translateY(-100%);
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
}
/* IMPORTANT: #status must stay OUTSIDE any element that has (or ever
   gets) a CSS transform applied - a transform on an ancestor creates a
   new containing block for position:fixed descendants, so instead of
   covering the true viewport, this would get pinned to that ancestor's
   box instead. This has already happened once (the page-load fade-in
   animation on .page-fade-in's children broke this exact way) - see
   templates/index.html, where #status is deliberately placed as a
   sibling after .page-fade-in closes, not nested inside it. */

.status.status-visible {
  transform: translateY(0);
  pointer-events: auto;
}

/* Same white background + rounded border as .page-frame, applied only
   to the loading panel - scoped by #status so the shared base
   .status-panel rule (used by the blue "How it works" panel too) stays
   untouched. */
#status .status-panel {
  background: var(--paper);
  border: 1px solid var(--line);
}

.explainer-section {
  display: flex;
  justify-content: center;
  padding: 0 1.5rem;
  margin: 2.5rem 0;
}

.explainer-section .status-panel {
  max-height: none;
  overflow-y: visible;
  align-items: stretch;
  padding: 2.5rem 2.5rem 2rem;
}

/* Reformatted for this context specifically: diagram sits at left,
   title + paragraph sit in a left-aligned column to its right - this
   scopes a side-by-side layout to .explainer-section only, so the
   loading overlay's own centered/stacked layout is untouched. */
.explainer-top {
  display: flex;
  align-items: center;
  gap: 2.5rem;
}

.explainer-section .hub-anim.hub-anim-static {
  flex-shrink: 0;
  width: auto;
}

.explainer-section .status-intro {
  display: block;
  text-align: left;
  flex: 1;
  min-width: 0;
  max-width: 640px;
  margin: 0;
}

.explainer-section .status-kicker {
  justify-content: flex-start;
  margin-bottom: 0.9rem;
  font-size: 1.7rem;
}

.explainer-section .status-intro-text {
  max-width: 58ch;
  margin: 0;
  font-size: 1.05rem;
}

@media (max-width: 700px) {
  .explainer-top {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.5rem;
  }
}

/* Static (non-animated) version of the hub-and-spoke art: same markup as
   the loading overlay's animated version, but with the animations
   stripped and end-state values applied directly - fully drawn lines,
   fully visible satellites, permanently, no motion. Scoped to this
   modifier class only, so the loading overlay's actual animated version
   is completely unaffected.

   Uses compound selectors (.hub-anim.hub-anim-static, not just
   .hub-anim-static alone) specifically so these overrides win on CSS
   specificity regardless of where they fall in the file relative to the
   base .hub-anim rules - .hub-anim-static alone would have equal
   specificity to .hub-anim and silently lose depending on source order,
   which is exactly what happened on the first pass here (caught before
   shipping by checking rule order directly, not by assuming it worked). */
.hub-anim.hub-anim-static {
  justify-content: flex-start;
  padding-left: 1rem;
}

.hub-anim.hub-anim-static svg {
  width: min(38vw, 220px);
}

.hub-anim.hub-anim-static .spoke-line {
  animation: none;
  stroke-dashoffset: 0;
}

.hub-anim.hub-anim-static .satellite {
  animation: none;
  opacity: 1;
  transform: scale(1);
}

.explainer-steps {
  width: 100%;
  display: flex;
  justify-content: flex-start;
  gap: 2.5rem;
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid rgba(0, 0, 0, 0.15);
}

.explainer-steps span {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--ink);
}

.status-panel {
  width: min(94vw, 980px);
  max-height: 92vh;
  overflow-y: auto;
  background: #6492b7;
  border-radius: 28px;
  padding: 2.5rem 2.5rem 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.status-intro {
  width: 100%;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1.5rem;
  align-items: start;
  margin-bottom: 1.5rem;
}

@media (max-width: 620px) {
  .status-intro { grid-template-columns: 1fr; }
}

.status-kicker {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 1.5rem;
  color: var(--ink);
  margin: 0;
  white-space: nowrap;
}

.status-kicker-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.7rem;
  height: 1.7rem;
  border-radius: var(--pill-radius);
  background: var(--ink);
  color: var(--accent);
  font-size: 0.9rem;
  flex-shrink: 0;
}

.status-intro-text {
  margin: 0;
  color: var(--ink);
  opacity: 0.78;
  font-size: 0.95rem;
  line-height: 1.55;
  max-width: 48ch;
}

/* ---------- Hub-and-spoke loading animation ---------- */
/* Represents the actual pipeline (collect -> analyze -> verify -> report),
   not decoration for its own sake. Each satellite <g> is nested inside a
   static positioning <g transform="translate(...)"> specifically so the
   CSS transform animation on the inner element never collides with the
   SVG attribute-based translate - animating CSS transform on an element
   that also carries a transform="" attribute overrides that attribute
   entirely in modern browsers, which would snap satellites to the wrong
   position instead of just scaling them.

   Runs on a clean 4-second cycle split into even quarters (0/1/2/3s
   delays) so each satellite's active window lines up exactly with its
   matching caption phase below - the icon and the word "Analyzing"
   light up together, not independently. */

.hub-anim {
  display: flex;
  justify-content: center;
  width: 100%;
  margin: 0.5rem 0;
}

.hub-anim svg {
  width: min(51vw, 323px);
  height: auto;
}

.hub-anim .hub {
  fill: var(--ink);
  transform-origin: 100px 100px;
  animation: hub-pulse 1.6s ease-in-out infinite;
}

.hub-anim .spoke-line {
  stroke: var(--ink);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 70;
  stroke-dashoffset: 70;
  animation: spoke-draw 4s ease-in-out infinite;
}

.hub-anim .satellite circle:first-child { fill: var(--ink); }

.hub-anim .satellite {
  opacity: 0;
  transform: scale(0.4);
  animation: satellite-pop 4s ease-in-out infinite;
}

@keyframes hub-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.12); }
}

@keyframes spoke-draw {
  0% { stroke-dashoffset: 70; }
  15% { stroke-dashoffset: 0; }
  85% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 70; }
}

@keyframes satellite-pop {
  0% { opacity: 0; transform: scale(0.4); }
  15% { opacity: 1; transform: scale(1); }
  85% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.4); }
}

.hub-anim-caption {
  position: relative;
  width: 100%;
  max-width: 280px;
  height: 1.4rem;
  margin: 0.5rem 0 1.2rem;
}

.hub-anim-caption .phase {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0;
}

.hub-anim-caption .phase.active {
  animation: phase-pulse 1.4s ease-in-out infinite;
}

@keyframes phase-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

.progress-track {
  width: 100%;
  max-width: 320px;
  height: 6px;
  background: var(--mist);
  border-radius: 999px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0%;
  background: var(--ink);
  border-radius: 999px;
  transition: width 0.25s ease-out;
}

/* ---------- Results ---------- */

.results { margin-top: 2.5rem; }

.results-header h2 {
  font-family: var(--font-sans);
  font-size: 1.4rem;
  margin: 0 0 0.35rem;
}

.disclaimer {
  font-size: 0.85rem;
  color: var(--ink-soft);
  max-width: 60ch;
  margin: 0 0 1.5rem;
}

.email-report-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin: 0 0 1.75rem;
  padding: 0.9rem 1rem;
  background: var(--mist);
  border-radius: 12px;
}

.email-report-row input[type="email"] {
  flex: 1 1 220px;
  min-width: 0;
}

.email-report-status {
  font-size: 0.82rem;
  font-family: var(--font-sans);
}

.email-report-status.success { color: #2d7a3e; }
.email-report-status.error { color: var(--accent-deep); }

.dimension-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
}

@media (max-width: 620px) {
  .dimension-row { grid-template-columns: 1fr; }
}

.dimension-block:first-child {
  border-right: 1px solid var(--line);
  padding-right: 1.5rem;
}

@media (max-width: 620px) {
  .dimension-block:first-child {
    border-right: none;
    border-bottom: 1px solid var(--line);
    padding-right: 0;
    padding-bottom: 1rem;
    margin-bottom: 1rem;
  }
}

.dimension-label {
  font-family: var(--font-sans);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  margin: 0.8rem 0 0.3rem;
}

.card-body .dimension-label:first-of-type {
  margin-top: 0.6rem;
}

.learn-why-intro {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  color: var(--ink-soft);
  margin: 0 0 0.5rem;
  line-height: 1.4;
}

.listing-summary {
  background: var(--paper-raised);
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  padding: 1.2rem 1.4rem;
  margin-bottom: 1.5rem;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}

.listing-summary.visible {
  opacity: 1;
  transform: translateY(0);
}

.listing-summary-inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.listing-summary-score {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.listing-summary-score .score-num {
  font-family: var(--font-sans);
  font-size: 2.2rem;
  font-weight: 700;
}

.listing-summary-note {
  font-size: 0.82rem;
  color: var(--ink-soft);
  margin: 0.5rem 0 0;
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1.25rem;
}

.card {
  background: var(--paper-raised);
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  overflow: hidden;
  position: relative;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}

.card.visible {
  opacity: 1;
  transform: translateY(0);
}

.card-thumb {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--line);
  overflow: hidden;
}

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

.card-thumb .stamp {
  position: absolute;
  top: 0.6rem; right: 0.6rem;
}

.card-body { padding: 0.9rem 1rem 1.1rem; }

.card-filename {
  font-family: var(--font-sans);
  font-size: 0.72rem;
  color: var(--ink-soft);
  word-break: break-all;
  margin: 0 0 0.5rem;
}

.score-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}

.score-num {
  font-family: var(--font-sans);
  font-size: 1.8rem;
  font-weight: 700;
}

.score-label {
  font-family: var(--font-sans);
  font-size: 0.72rem;
  color: var(--ink-soft);
  text-transform: uppercase;
}

.score-bar-track {
  width: 100%;
  height: 8px;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: 999px;
  overflow: hidden;
  margin: 0.4rem 0 0.9rem;
}

.score-bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.3s ease-out;
}

.learn-why {
  margin-top: 0.8rem;
  border-top: 1px solid var(--line);
  padding-top: 0.6rem;
}

.learn-why summary {
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.learn-why summary::-webkit-details-marker { display: none; }

.learn-why summary::before {
  content: "▸";
  font-size: 0.7rem;
  transition: transform 0.15s ease-out;
}

.learn-why[open] summary::before {
  transform: rotate(90deg);
}

.learn-why-content {
  padding-top: 0.7rem;
}

.notes {
  list-style: none;
  padding: 0;
  margin: 0;
  border-top: 1px solid var(--line);
  padding-top: 0.6rem;
}

.notes li {
  font-size: 0.82rem;
  color: var(--ink-soft);
  margin-bottom: 0.4rem;
  padding-left: 1rem;
  position: relative;
}

.notes li::before {
  content: "—";
  position: absolute;
  left: 0;
  color: var(--accent);
}

/* ---------- Errors ---------- */

.errors {
  margin-top: 1.5rem;
  padding: 1rem 1.2rem;
  background: var(--paper-raised);
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  font-size: 0.85rem;
}

/* ---------- About page ---------- */

.about-section {
  max-width: 680px;
  margin: 0 auto 2.5rem;
}

.about-section h2 {
  font-family: var(--font-sans);
  font-size: 1.7rem;
  margin: 0 0 1.2rem;
}

.about-body {
  font-size: 1.02rem;
  line-height: 1.7;
  color: var(--ink);
}

.about-nav {
  max-width: 680px;
  margin: 0 auto 2rem;
}

.footer-link {
  color: var(--accent);
  text-decoration: underline;
  font-size: 0.85rem;
}

/* ---------- Footer ---------- */

footer {
  position: relative;
  z-index: 1;
  border-top: 1px solid var(--line);
  padding: 1.5rem;
  text-align: center;
}

footer p {
  max-width: 60ch;
  margin: 0 auto;
  font-size: 0.78rem;
  color: var(--ink-soft);
}

@media (max-width: 620px) {
  .site-nav-inner { flex-wrap: wrap; row-gap: 0.75rem; }
  .site-nav-links { order: 3; width: 100%; gap: 1.25rem; }
  .stamp { justify-self: start; }
}

/* ---------- Landing page ---------- */
/* Standalone full-bleed marketing page (templates/landing.html), separate
   from the .page-frame card layout used by the rest of the site - the
   mockup this is built from has no shared nav/footer, just the photo
   with the wordmark/login/CTA floating on top, so it isn't scoped inside
   .page-frame the way every other page is.

   The headline, subhead, and feature list are baked directly into
   static/img/landing-hero.jpg (flat pixels, not live HTML/CSS text) -
   the wordmark, Log in link, and Get started button are the only real
   DOM elements layered on top. Because of that, the Get started button
   is positioned by percentage to land on the blank space already in the
   image, rather than centered by flex - this lines up well at typical
   desktop/laptop widths but can drift at unusual aspect ratios (very
   short/wide or very tall/narrow viewports) since background-size:cover
   crops differently at those extremes - worth a check on a phone once
   this is live.

   Fade-in is staged in two beats: the photo (.landing-hero itself) fades
   in first, then the topbar/CTA follow half a beat later, so the page
   reads as "photo arrives, then controls appear" rather than everything
   popping in at once. */
.landing-body {
  margin: 0;
  background: var(--page-backdrop);
  color: var(--ink);
  font-family: var(--font-sans);
  /* Padding on all sides so .landing-page-frame reads as a card floating
     on the backdrop, same as every other page's .page-frame - without
     this the frame would touch the top/bottom of the viewport even
     though it's capped to 1240px and centered horizontally. */
  padding: 2rem 1.5rem;
  box-sizing: border-box;
  min-height: 100vh;
}

@keyframes landing-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* .landing-page-frame reuses .page-frame's white background/border/
   radius as-is (see "Page frame" section above) - this modifier just
   accounts for the hero's min-height needing to subtract the body
   padding above, so the whole thing still fits ~one viewport tall. */
.landing-hero {
  position: relative;
  min-height: calc(100vh - 4rem);
  width: 100%;
  background-image: url('/static/img/landing-hero.jpg');
  background-size: cover;
  background-position: center;
  opacity: 0;
  animation: landing-fade-in 1s ease-out forwards;
}

@media (max-width: 640px) {
  .landing-body { padding: 0; }
  .landing-hero { min-height: 100vh; }
}

.landing-topbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2rem 2.5rem 0;
  opacity: 0;
  animation: landing-fade-in 0.8s ease-out forwards;
  animation-delay: 0.5s;
}

.landing-wordmark {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: 0.12em;
  color: var(--ink);
}

.landing-login {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.landing-login:hover {
  border-bottom-color: var(--ink);
}

.landing-cta {
  position: absolute;
  top: 58%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--font-sans);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.85rem;
  padding: 0.85rem 2rem;
  opacity: 0;
  animation: landing-fade-in 0.8s ease-out forwards;
  animation-delay: 0.5s;
}

@media (max-width: 620px) {
  .landing-topbar { padding: 1.5rem; }
}

/* ---------- Tutorial page ---------- */
/* Video sits in the same white-bordered rounded panel as the loading
   page's own #status .status-panel (white bg + var(--line) border,
   rather than the blue "How it works" variant) - a dedicated class
   rather than reusing #status .status-panel directly, since this page
   isn't nested inside #status and doesn't need that panel's fixed-
   overlay positioning or scroll behavior. */
.tutorial-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.75rem;
  padding: 3rem 1.5rem 4rem;
}

.tutorial-heading {
  font-weight: 700;
  font-size: 2.4rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0;
}

.tutorial-panel {
  width: min(94vw, 900px);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 28px;
  padding: 1.5rem;
  display: flex;
  justify-content: center;
}

.tutorial-video {
  width: 100%;
  max-width: 820px;
  border-radius: 14px;
  display: block;
}

/* Reuses .landing-cta for the exact same look as the landing page's
   "Get started" button (uppercase, letter-spaced, same padding/pill
   shape via .btn.primary) - but .landing-cta is absolutely positioned
   and pre-faded for the landing page's full-bleed photo layout, neither
   of which applies here, so both get reset back to normal in-flow
   placement. */
.tutorial-skip {
  position: static;
  transform: none;
  opacity: 1;
  animation: none;
}
