:root {
  --bg: #0d1117;
  --hero-bg: #05010f;
  --text: #f0f6fc;
  --muted: #9198a1;
  --border: #3d444d;
  --green: #29903b;
  --green-hover: #2ea043;
  --accent: #a371f7;
  --panel: #0d1117;
  /* Night sky for the scrolling content. --sky-horizon is sampled straight
     from the top edge of the footer video (assets/footer.webp) so the content's
     gradient hands off seamlessly to the video's sky — keep them in sync if the
     footer art is ever re-generated. --sky-top is a deeper zenith tone that
     bridges the purple-black hero. */
  --sky-top: #080d18;
  --sky-mid: #0a1220;
  --sky-horizon: #0b121f;
  --focus: #4493f8;
  /* Dark scrollbars + native controls (the site has no light mode) */
  color-scheme: dark;
}

/* Marketing typeface — self-hosted so first paint doesn't wait on a CDN */
@font-face {
  font-family: "Mona Sans";
  font-style: normal;
  font-display: swap;
  font-weight: 200 900;
  src: url("fonts/mona-sans-latin-wght.woff2") format("woff2-variations");
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* GitHub-style keyboard focus ring (replaces the UA default, which is
   near-invisible on the dark canvas). Blue reads on both the dark page
   and the white menu sheets. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

html { scroll-behavior: smooth; }

body {
  font-family: "Mona Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
  background: #000;
  color: var(--text);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* ===== Header ===== */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  background: rgba(5, 1, 15, 0.15);
  backdrop-filter: blur(10px);
  transition: background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
}
.site-header.scrolled {
  background: rgba(1, 4, 9, 0.85);
  box-shadow: 0 1px 12px rgba(0, 0, 0, 0.4);
}
/* "Start for Free" click: the top menu slides up out of view during phase 1. */
.site-header.header-leaving {
  pointer-events: none;
  animation: headerUp 0.8s cubic-bezier(0.7, 0, 0.84, 0) forwards;
}
@keyframes headerUp {
  0%   { transform: translateY(0) scale(1); }
  100% { transform: translateY(-150%) scale(1.6); }
}
.nav {
  position: relative;
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: 1280px;
  margin: 0 auto;
  padding: 12px 24px;
}
.logo { color: var(--text); display: flex; }
/* Both wordmarks ship on every page (see partials/header). The white mark is the default (the dark
   landing + the dark reading theme); the dark+green mark shows only on theme-adaptive pages resolved
   light (see the theme-adaptive section below). */
.logo-img { height: 26px; width: auto; display: block; }
.logo-img--light { display: none; }
.nav-links {
  display: flex;
  list-style: none;
  gap: 4px;
  margin-left: 8px;
}
.nav-links li { position: relative; display: flex; align-items: center; }
/* The menu-sheet CTA only exists on mobile (see max-width: 720px block).
   Scoped selector so it outranks the `.nav-links li` display rules. */
.nav-links .nav-cta { display: none; }
/* First menu button drops its left padding so the row lines up with the page
   gutter. Desktop row only — the mobile sheet keeps uniform item padding. */
@media (min-width: 721px) {
  .nav-links li:first-child .nav-item { padding-left: 0; }
}
.nav-item {
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 5px;
  background: none;
  border: none;
  border-radius: 6px;
  padding: 6px 8px;
  text-decoration: none;
  transition: color 0.15s ease, background 0.15s ease;
}
/* GitHub-style hover: the label only dims — no background pill. */
.nav-item:hover { color: var(--muted); }
.has-caret::after {
  content: "";
  width: 6px; height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  opacity: 0.7;
  transition: translate 0.15s ease;
}
/* GitHub-style caret: keeps pointing down, just nudges ~1px lower while the
   menu is open/hovered (mobile overrides this with an accordion rotate). */
.has-dropdown.open .has-caret::after { translate: 0 1px; }

/* ===== Mega-menu dropdowns ===== */
/* Submenus are intentionally empty for now — one muted coming-soon line. */
.mega-empty {
  margin: 0;
  color: #59636e;
  font-size: 14px;
  white-space: nowrap;
}
.mega {
  position: absolute;
  top: calc(100% + 12px);
  left: 0;
  display: flex;
  gap: 40px;
  background: #fff;
  color: #1f2328;
  border-radius: 12px;
  padding: 24px 28px;
  box-shadow: 0 12px 34px rgba(1, 4, 9, 0.45);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
  z-index: 10;
}
.mega--right { left: auto; right: 0; }
.has-dropdown.open .mega { opacity: 1; visibility: visible; transform: translateY(0); }
.mega::before {
  content: "";
  position: absolute;
  top: -12px; left: 0; right: 0;
  height: 12px;
}
.mega-col {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 232px;
}
.mega-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 8px;
  border-radius: 8px;
  text-decoration: none;
  color: inherit;
  transition: background 0.15s ease;
}
.mega-item:hover { background: #f3f4f6; }
.mega-icon {
  flex-shrink: 0;
  width: 22px; height: 22px;
  margin-top: 1px;
  color: #1f2328;
}
.mega-text { display: flex; flex-direction: column; }
.mega-text strong { font-size: 14px; font-weight: 600; color: #1f2328; }
.mega-text small { font-size: 13px; color: #59636e; margin-top: 2px; }

/* ===== Mobile toggle (hidden on desktop) ===== */
.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 40px; height: 40px;
  margin-left: auto;
  background: none;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  padding: 0 9px;
}
.nav-toggle span {
  display: block;
  height: 2px;
  background: var(--text);
  border-radius: 2px;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
}
.btn-text {
  color: var(--text);
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  transition: color 0.15s ease;
}
.btn-text:hover { color: var(--muted); }
.btn-outline {
  color: var(--text);
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 12px;
  background: rgba(255,255,255,0.05);
  transition: border-color 0.15s ease;
}
.btn-outline:hover { border-color: var(--muted); }

/* ===== Language switcher ===== */
/* Desktop: a globe dropdown pill in the bottom .footer-bar. It reuses the
   .has-dropdown/.nav-item hooks so the shared dropdown JS drives it; .lang-menu
   is a compact right-aligned cousin of .mega that opens UPWARD in the footer
   (see .footer-bar overrides below). The mobile sheet gets a separate
   .lang-mobile pill row instead (see the 720px block). */
.lang-switcher { position: relative; display: flex; }
/* Plain nav-item styling (inherited): white text, no background/border, dims
   on hover — just globe + current language name + caret. */
.lang-btn { gap: 6px; padding-right: 0; }
.lang-current { font-size: 14px; font-weight: 600; line-height: 1; }
.lang-globe { display: block; }
.lang-menu {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  min-width: 160px;
  white-space: nowrap;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: #010409;
  color: var(--text);
  border-radius: 12px;
  padding: 8px;
  box-shadow: 0 12px 34px rgba(1, 4, 9, 0.6);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
  z-index: 10;
}
/* Hover bridge across the gap to the trigger, same as .mega::before. */
.lang-menu::before {
  content: "";
  position: absolute;
  top: -12px; left: 0; right: 0;
  height: 12px;
}
.has-dropdown.open .lang-menu { opacity: 1; visibility: visible; transform: translateY(0); }
.lang-menu .lang-option {
  position: relative;
  padding: 6px 10px 6px 30px;
  border-radius: 8px;
  font-size: 14px;
  color: var(--text);
  text-decoration: none;
  transition: background 0.15s ease;
}
.lang-menu .lang-option:hover { background: rgba(255, 255, 255, 0.07); }
.lang-menu .lang-option[aria-current="true"] { font-weight: 600; }
.lang-menu .lang-option[aria-current="true"]::before {
  content: "";
  position: absolute;
  left: 11px; top: 50%;
  width: 9px; height: 5px;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-70%) rotate(-45deg);
}
/* The sheet row only exists on mobile. Scoped to outrank `.nav-links li`. */
.nav-links .lang-mobile { display: none; }

/* ===== Bottom bar (overlaid on the bottom of the footer video) ===== */
/* Mirrors the top menu: pinned to the video's bottom edge with the header's
   translucent hero background + blur (see .site-header), so it reads as the
   footer twin of the nav floating over the hero. */
.footer-bar {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  z-index: 3;
  background: rgba(5, 1, 15, 0.15);
  backdrop-filter: blur(10px);
  padding: 18px 0;
}
/* Same geometry as .nav so the row lines up with the header: the legal block's
   left edge sits under the logo (24px gutter, matching .nav's padding), and the
   language pill's right edge under "Sign up" (24px padding). */
.footer-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px 24px;
}
.footer-legal {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
}
.footer-link {
  color: var(--muted);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.footer-link { text-decoration: none; transition: color 0.15s ease; }
.footer-link:hover { color: var(--text); }
/* The legal page currently being viewed — full colour + bold so it reads as active in both themes
   (no accent colour: green sits poorly over the day footer video). */
.footer-link.is-active {
  color: var(--text);
  font-weight: 700;
}
.footer-right { display: flex; align-items: center; gap: 14px; }
.footer-social { display: flex; align-items: center; gap: 2px; }
.footer-ico {
  display: flex;
  padding: 6px;
  border-radius: 8px;
  color: var(--muted);
  transition: color 0.15s ease, background 0.15s ease;
}
.footer-ico:hover { color: var(--text); background: rgba(255, 255, 255, 0.08); }
/* The bar sits at the very bottom, so its language menu opens UPWARD. */
.footer-bar .lang-menu { top: auto; bottom: calc(100% + 12px); transform: translateY(6px); }
.footer-bar .lang-menu::before { top: auto; bottom: -12px; }
/* Caret points UP, toward the menu that opens above the pill. */
.footer-bar .has-caret::after { transform: rotate(-135deg) translateY(-2px); }
/* On hover/open it nudges UP (toward the menu), mirroring the nav caret's
   downward nudge. */
.footer-bar .has-dropdown.open .has-caret::after { translate: 0 -1px; }

/* ===== Theme selector ===== */
/* system / light / dark segmented control in the footer bar, just before the language switcher.
   Wired + persisted by the inline theme script in marketing::layout (writes the shared `appearance`
   cookie). Icons inherit the footer's muted/text tokens, so it adapts with the footer chrome; the
   light-theme footer overrides live in the theme-adaptive section below. */
.theme-switcher {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
/* Matches the social icons (.footer-ico): 20px glyph, 6px pad, 8px radius, no wrapper border. */
.theme-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px;
  border: none;
  border-radius: 8px;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.theme-option svg { display: block; }
.theme-option:hover { color: var(--text); background: rgba(255, 255, 255, 0.08); }
.theme-option.is-active { color: var(--text); background: rgba(255, 255, 255, 0.12); }
.theme-option:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* Thin vertical separators between the footer-right groups: social | theme | language. */
.footer-sep {
  flex: none;
  width: 1px;
  height: 18px;
  background: var(--border);
}

/* ===== Theme-adaptive pages (content + pricing) ===== */
/* The landing is locked dark; the content + pricing pages honour the resolved theme on
   <html data-appearance> (set by the inline theme script). The DARK theme is the default — those
   pages inherit the :root dark tokens above, only swapping the accent to the readable green. The
   LIGHT overrides re-scope the tokens white and repaint the SHARED chrome (header/footer glass,
   wordmark, language menu, back-to-top, theme switcher) for the day footer video. content.css and
   pricing.css add only their own body-content specifics on top. Scoped to the two adaptive body
   classes so the landing (neither class) never flips. */
:is(.blog-body, .pricing-body) {
  --accent: #3fb950;      /* readable green on the dark reading theme (vs the :root purple) */
  --border-hover: #59636e;
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) {
  --bg: #ffffff;
  --text: #1f2328;
  --muted: #59636e;
  --border: #d1d9e0;
  --panel: #ffffff;
  --hero-bg: #f6f8fa;
  --accent: #1a7f37;
  --border-hover: #8c959f;
}

/* Wordmark: dark+green on the light reading theme, white everywhere else. */
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .logo-img--dark { display: none; }
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .logo-img--light { display: block; }

/* The fixed header's translucent-dark glass reads as dirt on white — go SOLID white + a hairline
   (no transparency/blur on the light theme). */
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .site-header {
  background: #ffffff;
  backdrop-filter: none;
  border-bottom: 1px solid var(--border);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .site-header.scrolled {
  background: #ffffff;
  box-shadow: 0 1px 12px rgba(31, 35, 40, 0.08);
}

/* The light pages play the DAY footer video (light sky/grass): same glass alpha as the dark footer
   bar, just white; the video's feathered top edge resolves to the white page, not the dark sky. */
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .footer-bar {
  background: rgba(255, 255, 255, 0.15);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .site-footer {
  background: var(--bg);
}

/* Muted grey legal text + footer controls disappear against the day scene's grass — full text
   colour, with the hover dimming to muted (inverse of the dark footer). */
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .footer-link,
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .footer-ico,
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .theme-option {
  color: var(--text);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .footer-link:hover,
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .footer-ico:hover,
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .theme-option:hover {
  color: var(--muted);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .footer-ico:hover,
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .theme-option:hover {
  background: rgba(31, 35, 40, 0.06);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .theme-option.is-active {
  background: rgba(31, 35, 40, 0.1);
  color: var(--text);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .footer-sep {
  background: rgba(31, 35, 40, 0.2);
}

/* marketing.css hardcodes the language menu dark (#010409) — flip it white + a light hover. */
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .lang-menu {
  background: #ffffff;
  box-shadow: 0 12px 34px rgba(31, 35, 40, 0.18);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .lang-menu .lang-option:hover {
  background: rgba(31, 35, 40, 0.06);
}

/* The back-to-top button is dark by default (for the dark landing); on the light reading pages it
   reads as a light circle with a dark arrow + hairline. */
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .back-to-top {
  background: rgba(255, 255, 255, 0.92);
  color: var(--text);
  border: 1px solid var(--border);
  box-shadow: 0 6px 20px rgba(31, 35, 40, 0.14);
}
:root[data-appearance='light'] :is(.blog-body, .pricing-body, .home-body) .back-to-top:hover {
  background: #ffffff;
  border-color: var(--border-hover);
}

/* ===== Landing (.home-body) — light theme ===== */
/* The landing opts into the shared light tokens + chrome above; these re-skin the parts unique to
   the home page. On light, the HERO swaps to the day art (background-day.webp, torches hidden) but
   keeps its light (white) reading tokens; everything below the hero (rising content, feature
   sections, FAQ) turns light. The faux app-window demo screens stay dark in both themes, like code blocks. */

/* Behind everything, so an overscroll never flashes the night-black body. */
:root[data-appearance='light'] body.home-body { background: var(--bg); }

/* Hero keeps its light (white) reading tokens even on the day art — the .btn-ghost's white-alpha
   border/fill and .hero-sub's fixed light grey suit it. */
:root[data-appearance='light'] .home-body .hero {
  --text: #f0f6fc;
  --muted: #9198a1;
  --hero-bg: #05010f;
  color: var(--text);
}
/* Light theme swaps in the day hero art and drops the torches (a daylight scene). */
:root[data-appearance='light'] .home-body .hero-bg-night { display: none; }
:root[data-appearance='light'] .home-body .hero-bg-day { display: block; }
:root[data-appearance='light'] .home-body .hero-torch { display: none; }
/* The "rising content" is a night-sky gradient with a tiled starfield — flatten both to the page. */
:root[data-appearance='light'] .home-body .content {
  background: var(--bg);
  box-shadow: none;
}
:root[data-appearance='light'] .home-body .content::before {
  display: none;
}

/* Feature boxes: the same light --feature-* palette the content pages use. */
:root[data-appearance='light'] body.home-body .feature {
  --feature-text: #1f2328;
  --feature-muted: #59636e;
  --feature-chrome: #eaeef2;
  --feature-border: #d1d9e0;
  --feature-tab-active: #ffffff;
  --feature-tab-hover: rgba(31, 35, 40, 0.06);
  --feature-screen: #ffffff;
  --feature-shadow: 0 30px 60px rgba(31, 35, 40, 0.14);
}

/* Highlighter ink: the night glaze is a bright purple meant to glow against black — on white it
   washes out, so drop to a deeper purple (#8250df, the light-theme sibling of the :root accent). */
:root[data-appearance='light'] body.home-body .marker {
  --marker-ink: rgba(130, 80, 223, 0.34);
}

/* Recent-posts tiles: the night theme's white-alpha fill is invisible on a light page — flip it to
   a dark-alpha wash so the cards still read as tiles. */
:root[data-appearance='light'] body.home-body .posts {
  --post-card-bg: rgba(31, 35, 40, 0.04);
  --post-card-bg-hover: rgba(31, 35, 40, 0.07);
}

/* On phones the legal links and social icons stack — centre both rows instead of
   leaving them left-aligned from the desktop space-between row. */
@media (max-width: 600px) {
  .footer-inner {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 0 24px;
  }
  .footer-legal,
  .footer-right,
  .footer-social {
    justify-content: center;
  }
  /* The row stacks vertically here — the vertical separators would float between rows. */
  .footer-sep {
    display: none;
  }
}

/* ===== Hero (pinned) ===== */
/* Full-screen panels (hero, .panel, .site-footer) size with
   svh, NOT dvh: on mobile the URL bar collapsing/expanding changes dvh on
   every scroll direction flip, which made all five panels (and the document)
   resize mid-scroll — the cover-cropped hero/footer art visibly "zoom
   jumped" and the shrinking document yanked the scroll position around near
   the bottom. svh is the bar-visible height and never changes while
   scrolling. */
.hero {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  overflow: hidden;
  background: var(--hero-bg);
  z-index: 1;
}

/* Background image + torch overlay share one scaling frame so they stay
   locked together at every viewport size (replicates background-size: cover). */
.hero-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
}
.hero-bg-frame {
  --img-w: 1408;
  --img-h: 736;
  position: absolute;
  top: 50%;
  left: 50%;
  /* --bg-zoom (driven by script.js on scroll) displays the cave zoomed-in at
     rest (1.25) and eases to its natural cover size (1) as you scroll, so the
     dolly-out reveals more of the art while always filling the frame. */
  transform: translate(-50%, -50%) scale(var(--bg-zoom, 1.25));
  /* cover: take the larger of (fit-width) and (fit-height) — svh to match
     the hero's own (stable) height */
  width: max(100vw, 100svh * (var(--img-w) / var(--img-h)));
  height: max(100svh, 100vw * (var(--img-h) / var(--img-w)));
  transform-origin: center;
}
/* "Start for Free" click: zoom the whole background frame (cave + torches,
   which live inside it) to 350% from the centre over 1.5s. Animates the
   individual `scale` so it composes with the centring translate above. */
.hero-bg-frame.bg-expanding {
  animation: bgExpand 1.5s linear forwards;
}
/* Keep the centring translate in the same transform as the scale so the zoom
   stays anchored to the middle of the background (not a corner). */
@keyframes bgExpand {
  /* Start from the background's current scroll zoom (--bg-zoom) so the exit
     zoom continues smoothly from wherever the dolly-out left it, rather than
     jumping back to 1× first. */
  0%   { transform: translate(-50%, -50%) scale(var(--bg-zoom, 1.25)); }
  100% { transform: translate(-50%, -50%) scale(3.5); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-bg-frame.bg-expanding { animation-duration: 0.01s; }
}

/* ===== "Start for free" exit fade =====
   The final stretch of the exit zoom fades the screen to the app login
   page's background colour so the landing's last frame matches the app's
   first. The fade target mirrors the app's own theme resolution, so the two
   agree on which page the visitor is about to land on: an explicit in-app
   choice wins (the domain-wide `appearance` cookie the app's use-appearance
   hook writes, mirrored onto <html data-appearance> by script.js), and
   `system`/no cookie falls back to prefers-color-scheme. The two targets are
   the app's own --background: off-white oklch(0.99 0.004 128) light,
   off-black oklch(0.16 0.008 128) dark (resources/css/app.css — keep in
   sync). Timings are tuned to the 1.2s navigation dispatch in script.js. */
.login-glow-overlay {
  position: fixed;
  inset: 0;
  z-index: 999;
  pointer-events: none;
  visibility: hidden;
}
.login-glow-overlay.leaving {
  visibility: visible;
  animation: loginBgFadeLight 0.5s ease-in 0.7s forwards;
}
@media (prefers-color-scheme: dark) {
  .login-glow-overlay.leaving { animation-name: loginBgFadeDark; }
}
/* An explicit in-app choice overrides the OS preference — higher specificity
   than the rules above, so it wins whichever way the media query resolved. */
:root[data-appearance='light'] .login-glow-overlay.leaving { animation-name: loginBgFadeLight; }
:root[data-appearance='dark'] .login-glow-overlay.leaving { animation-name: loginBgFadeDark; }
@keyframes loginBgFadeLight {
  from {
    background: rgba(251, 252, 250, 0);
    background: oklch(0.99 0.004 128 / 0);
  }
  to {
    background: rgb(251, 252, 250);
    background: oklch(0.99 0.004 128);
  }
}
@keyframes loginBgFadeDark {
  from {
    background: rgba(12, 14, 10, 0);
    background: oklch(0.16 0.008 128 / 0);
  }
  to {
    background: rgb(12, 14, 10);
    background: oklch(0.16 0.008 128);
  }
}
@media (prefers-reduced-motion: reduce) {
  .login-glow-overlay.leaving { animation-duration: 0.01s; animation-delay: 0s; }
}
/* The <picture> wrapper (art swap for portrait phones) must not create a
   layout box of its own — the img sizes against the frame. */
.hero-bg-frame picture { display: contents; }
.hero-bg-img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: fill;
}
/* Night art is the default; the day variant is revealed on the light theme (see below). */
.hero-bg-day { display: none; }
.hero-torch {
  position: absolute;
  height: auto;
}

/* ===== Crystal sparkles ===== */
/* Twinkling glints overlaid on the foreground crystals. Positioned as a
   percentage of the frame, so they stay glued to the painted crystals. */
.crystal-sparkles { position: absolute; inset: 0; pointer-events: none; }
.sparkle {
  position: absolute;
  /* Per-sparkle inline vars: --x/--y position on the desktop art;
     the phone-portrait block below swaps in --mx/--my instead. */
  left: var(--x);
  top: var(--y);
  width: var(--sz, 16px);
  height: var(--sz, 16px);
  margin: calc(var(--sz, 16px) / -2) 0 0 calc(var(--sz, 16px) / -2);
  background: var(--tint, #fff);
  /* 4-point star */
  clip-path: polygon(
    50% 0%, 58% 42%, 100% 50%, 58% 58%,
    50% 100%, 42% 58%, 0% 50%, 42% 42%
  );
  filter: drop-shadow(0 0 5px var(--tint, #fff)) brightness(1.3);
  opacity: 0;
  will-change: transform, opacity;
  animation: sparkle var(--dur, 3.2s) ease-in-out var(--delay, 0s) infinite;
}
/* A soft round bloom under each star so the crystal appears to brighten */
.sparkle::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 240%; height: 240%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, var(--tint, #fff) 0%, transparent 65%);
  opacity: 0.5;
}
@keyframes sparkle {
  0%, 100%   { opacity: 0;    transform: scale(0.25); }
  4%         { opacity: 0.7;  transform: scale(1); }
  8%         { opacity: 0.12; transform: scale(0.7); }
  12%        { opacity: 0.65; transform: scale(1.05); }
  18%, 100%  { opacity: 0;    transform: scale(0.25); }
}
@media (prefers-reduced-motion: reduce) {
  .sparkle { animation: none; opacity: 0.55; }
}
/* Positioned/sized as a percentage of the frame = intrinsic image coords,
   so it stays glued to the painted torch at any screen size. */
.hero-torch-left {
  width: 9%;
  left: 12.1%;
  top: 5%;
  /* Two intersected masks: a tall radial that feathers the left/right sides,
     and a vertical gradient that fades the top (glow) generously but the
     bottom only in a thin strip near the border. */
  -webkit-mask-image:
    radial-gradient(58% 120% at 46% 50%, #000 55%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 20%, #000 93%, transparent 100%);
  mask-image:
    radial-gradient(58% 120% at 46% 50%, #000 55%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 20%, #000 93%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.hero-torch-right {
  width: 9%;
  left: 76%;
  top: 5%;
  -webkit-mask-image:
    radial-gradient(58% 120% at 54% 50%, #000 55%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 20%, #000 86%, transparent 100%),
    radial-gradient(52% 49% at 100% 100%, transparent 14%, #000 67%);
  mask-image:
    radial-gradient(58% 120% at 54% 50%, #000 55%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 20%, #000 86%, transparent 100%),
    radial-gradient(52% 49% at 100% 100%, transparent 14%, #000 67%);
  -webkit-mask-composite: source-in, source-in;
  mask-composite: intersect;
}
.hero-inner {
  /* Base lift, shared with the JS parallax (which writes
     translateY(calc(var(--hero-lift) - N vh)) inline) so media queries can
     reposition the hero without fighting the scroll handler. */
  --hero-lift: -9vh;
  position: relative;
  z-index: 3;
  padding: 0 24px;
  transform: translateY(var(--hero-lift));
  transform-origin: center;
  /* Kept on its own GPU layer (also transformed on scroll) so the CTA
     fly-through animates smoothly without re-rasterising each frame. */
  will-change: transform;
}
/* "Start for Free" click: collapse the hero content to its centre and fade.
   Animates the individual `scale` property so it composes with the parallax
   `transform` set in JS instead of overriding it. */
.hero-inner.hero-leaving {
  pointer-events: none;
  /* Camera flies forward through the text: it grows past the viewport (faster
   than the background, for parallax depth) rather than shrinking away. Same
   ease-in as the background zoom so they move as one. */
  animation: heroCollapse 0.8s cubic-bezier(0.7, 0, 0.84, 0) forwards;
  /* Promote to a GPU layer so the scale is composited instead of re-rasterising
   the (growing) text every frame. */
  will-change: transform, opacity;
  backface-visibility: hidden;
}
@keyframes heroCollapse {
  0%   { scale: 1; opacity: 1; }
  65%  { opacity: 1; }
  100% { scale: 7; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-inner.hero-leaving { animation-duration: 0.01s; }
}
.hero-title {
  font-size: clamp(36px, 5vw, 60px);
  /* 500 keeps the 60px title optically heavier than the 48px/600 section
     h2s (425 read lighter, inverting the hierarchy). */
  font-weight: 500;
  letter-spacing: -0.035em;
  line-height: 1.08;
  margin-bottom: 24px;
  text-wrap: balance;
}
.hero-sub {
  font-size: 18px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: #d1d7e0;
  margin-bottom: 32px;
}
.hero-cta {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.cta-note {
  font-size: 14px;
  color: var(--muted);
  margin: 14px 0 0;
}
.btn-green {
  display: inline-block;
  background: var(--green);
  color: #fff;
  border: none;
  border-radius: 10px;
  padding: 16px 22px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  line-height: 1;
  transition: background-color 0.15s ease;
}
.btn-green:hover { background: var(--green-hover); }
.is-disabled { pointer-events: none; cursor: default; }
/* Secondary CTA: scrolls to the content panel */
.btn-ghost {
  display: inline-flex;
  align-items: center;
  color: var(--text);
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.06);
  border-radius: 10px;
  padding: 16px 22px;
  font-size: 16px;
  font-weight: 600;
  text-decoration: none;
  line-height: 1;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.btn-ghost:hover { border-color: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.1); }

/* floating scene */
.hero-scene { position: absolute; inset: 0; z-index: 1; }
.goblin {
  position: absolute;
  /* Mobile stays at ~44vw; wider screens grow the goblin up to 330px. */
  width: min(44vw, clamp(240px, 18vw, 330px));
  height: auto;
  /* Sprites are taller than wide, so width-only sizing lets them eat short
     viewports — cap the height too (replaced elements keep their aspect
     ratio when max-height clamps, shrinking the width to match). */
  max-height: 52vh;
  object-fit: contain;
  filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.45));
}
.goblin-center { left: 50%; bottom: 2%; transform: translateX(-50%); }
/* "Start for Free" click: goblin slides straight down out of the hero
   (the hero clips it with overflow: hidden). */
.goblin-center.goblin-exit {
  animation: goblinExit 0.8s linear forwards;
}
@keyframes goblinExit {
  0%   { transform: translateX(-50%) translateY(0) scale(1); }
  100% { transform: translateX(-50%) translateY(180%) scale(1.8); }
}
@media (prefers-reduced-motion: reduce) {
  .goblin-center.goblin-exit { animation-duration: 0.01s; }
}
.stars {
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(1.5px 1.5px at 20% 70%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 65% 80%, #fff, transparent),
    radial-gradient(1px 1px at 45% 60%, #fff, transparent),
    radial-gradient(1px 1px at 75% 65%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 30% 85%, #fff, transparent);
  opacity: 0.6;
}

/* ===== Rising content ===== */
/* The content is one continuous night sky: a document-flow gradient (NOT
   fixed — it must scroll with the page) whose final stop is the footer video's
   own sky colour (--sky-horizon). Because <footer> is the next sibling after
   </main>, this element's bottom edge sits on the video's first pixel row, so
   the gradient flows straight into the night sky of the footer scene. */
.content {
  position: relative;
  z-index: 2;
  /* Black at the top edge dissolving into the blue night sky, which then runs
     down into the footer video's sky (--sky-horizon). */
  background: linear-gradient(
    180deg,
    #000 0%,
    var(--sky-top) 18%,
    var(--sky-mid) 50%,
    var(--sky-horizon) 100%
  );
  box-shadow: 0 -20px 60px rgba(0,0,0,0.6);
}
/* Faint tiled starfield over the whole scroll so the sky reads as sky. Tiles a
   small star cell (background-size) up the full content height; the rounded top
   is inherited so stars never spill past the corners. Purely decorative. */
.content::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  background-image:
    radial-gradient(1.5px 1.5px at 18% 24%, rgba(255,255,255,0.9), transparent),
    radial-gradient(1px 1px at 62% 12%, rgba(255,255,255,0.7), transparent),
    radial-gradient(1px 1px at 82% 46%, rgba(255,255,255,0.8), transparent),
    radial-gradient(1.5px 1.5px at 40% 68%, rgba(255,255,255,0.65), transparent),
    radial-gradient(1px 1px at 8% 82%, rgba(255,255,255,0.7), transparent),
    radial-gradient(1px 1px at 72% 88%, rgba(255,255,255,0.6), transparent),
    radial-gradient(1px 1px at 14% 8%, rgba(255,255,255,0.6), transparent),
    radial-gradient(1.5px 1.5px at 52% 30%, rgba(255,255,255,0.75), transparent),
    radial-gradient(1px 1px at 90% 20%, rgba(255,255,255,0.6), transparent),
    radial-gradient(1px 1px at 4% 52%, rgba(255,255,255,0.65), transparent),
    radial-gradient(1px 1px at 30% 44%, rgba(255,255,255,0.55), transparent),
    radial-gradient(1px 1px at 48% 56%, rgba(255,255,255,0.5), transparent),
    radial-gradient(1.5px 1.5px at 68% 62%, rgba(255,255,255,0.7), transparent),
    radial-gradient(1px 1px at 88% 74%, rgba(255,255,255,0.6), transparent),
    radial-gradient(1px 1px at 24% 92%, rgba(255,255,255,0.55), transparent),
    radial-gradient(1px 1px at 58% 82%, rgba(255,255,255,0.55), transparent);
  background-size: 300px 300px;
  /* Sky is darkest (fewest visible stars) at the zenith, richer toward the
     horizon where it meets the video — fade the field in as you scroll down. */
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.75) 100%);
  mask-image: linear-gradient(180deg, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.75) 100%);
  opacity: 0.7;
}

/* ===== Parallax moon ===== */
/* Decorative night moon over the footer video's sky. marketing.js anchors it just above the
   footer and eases its translateY (parallax) so it lands on the footer sky at the bottom of every
   page. z-index 3 keeps it above the footer video (2) but below the header (100). Dark theme only —
   hidden on light, where the footer plays its day variant. The art is a pre-darkened opaque disc
   (no CSS filter). */
.moon {
  position: absolute;
  left: clamp(-24px, 3vw, 72px);
  top: 0;
  width: 90px;
  height: 90px;
  background: url("images/moon.webp") center / contain no-repeat;
  z-index: 3;
  pointer-events: none;
  will-change: transform;
}
@media (max-width: 720px) {
  .moon { width: 68px; height: 68px; left: -10px; }
}
@media (prefers-reduced-motion: reduce) {
  .moon { will-change: auto; }
}
/* Night moon only — hidden on the light theme (the footer plays its day variant there). */
:root[data-appearance='light'] :is(.home-body, .blog-body, .pricing-body) .moon { display: none; }

.panel {
  max-width: 1000px;
  margin: 0 auto;
  padding: 80px 24px;
  text-align: center;
  /* Full-screen section like the hero, content vertically centred. */
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.eyebrow {
  color: var(--accent);
  font-weight: 600;
  font-size: 14px;
  letter-spacing: 0.02em;
}
.panel-sub {
  color: var(--muted);
  font-size: 18px;
  line-height: 1.5;
  margin: 12px auto 0;
  max-width: 640px;
}

.panel h2 {
  font-size: clamp(32px, 4vw, 48px);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
  margin: 12px 0 16px;
  text-wrap: balance;
}

/* ===== FAQ (#faq) ===== */
/* github.com-pricing-style accordion on native <details>: hairline dividers,
   chevron flips when open. Content-sized — the full-screen .panel height
   would strand 8 collapsed rows in a sea of empty space. */
.faq {
  max-width: 820px;
  min-height: 0;
  padding-top: 40px;
  /* Modest — the closing CTA and recent-posts strip follow and own the run-out to the footer. */
  padding-bottom: 72px;
}
.faq h2 { margin-bottom: 28px; }
.faq-list {
  width: 100%;
  text-align: left;
  border-top: 1px solid var(--border);
}
.faq-item { border-bottom: 1px solid var(--border); }
.faq-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  list-style: none; /* hide the default disclosure triangle… */
  cursor: pointer;
  padding: 20px 4px;
  font-size: 17px;
  font-weight: 600;
  transition: color 0.15s ease;
}
.faq-q::-webkit-details-marker { display: none; } /* …and Safari's */
.faq-q:hover { color: var(--muted); }
.faq-caret {
  flex: none;
  color: var(--muted);
  transition: transform 0.2s ease;
}
.faq-item[open] .faq-caret { transform: rotate(180deg); }
.faq-a {
  color: var(--muted);
  font-size: 16px;
  line-height: 1.6;
  max-width: 720px;
  padding: 0 4px 20px;
}
@media (prefers-reduced-motion: reduce) {
  .faq-caret { transition: none; }
}

/* ===== Blog post cards (blog index + the landing's recent-posts strip) ===== */
/* These live HERE, not in content.css, because both pages render the same card markup and
   marketing.css is the one stylesheet both load (content::layout extends marketing::layout). The
   blog-only featured-card variants stay in content.css. Edit the card once, both surfaces follow. */
.blog-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: clamp(1.25rem, 3vw, 2rem);
}

.blog-card {
  border: 1px solid var(--border);
  overflow: hidden;
  background: color-mix(in srgb, var(--panel) 92%, white 8%);
  transition: border-color 0.15s ease;
}

/* Hover is a border-colour change only — deliberately no lift. (The prefers-reduced-motion
   opt-out that used to sit here went with it; there is no motion left to reduce.) */
.blog-card:hover {
  border-color: var(--border-hover);
}

.blog-card-link {
  display: flex;
  flex-direction: column;
  height: 100%;
  color: inherit;
  text-decoration: none;
}

.blog-card-media {
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.blog-card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Imageless posts get a quiet gradient panel with the title's initial */
.blog-card-media.is-placeholder,
.blog-featured-media.is-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(120% 120% at 18% 0%, color-mix(in srgb, var(--accent) 26%, transparent), transparent 60%),
    radial-gradient(120% 120% at 90% 100%, color-mix(in srgb, var(--green) 20%, transparent), transparent 55%),
    var(--hero-bg);
}

.blog-card-media.is-placeholder span,
.blog-featured-media.is-placeholder span {
  font-size: 4rem;
  font-weight: 800;
  color: color-mix(in srgb, var(--accent) 55%, transparent);
  user-select: none;
}

/* Grows to fill whatever height the tallest card in the row sets, so .blog-byline's `margin-top:auto`
   has slack to push against and the date/reading-time row lands on the card's bottom edge. Without
   the grow the body stays content-sized, the spare height pools BELOW it, and the byline rides up
   under a short excerpt — leaving the row's bylines unaligned. */
.blog-card-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: 0.5rem;
  padding: 1.25rem;
}

.blog-card-category,
.blog-post-category {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  text-decoration: none;
}

.blog-card-title {
  font-size: 1.25rem;
  font-weight: 650;
  line-height: 1.3;
}

.blog-card-excerpt {
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.55;
}

/* Author / date / reading-time row */
.blog-byline {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--muted);
  font-size: 0.825rem;
  padding-top: 0.5rem;
}

.blog-byline-author {
  color: var(--text);
  font-weight: 550;
}

.blog-byline-dot {
  opacity: 0.6;
}

.blog-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  flex-shrink: 0;
  background: color-mix(in srgb, var(--accent) 30%, var(--panel));
  color: var(--text);
  font-size: 0.75rem;
  font-weight: 700;
}

/* ===== Closing CTA (#get-started) ===== */
/* The last conversion prompt before the recent-posts strip: heading, the hero's own green button,
   and the hero's note line, over the bare night sky (no band art — it buried the starfield).
   Full-screen like the hero and .hiw: it adds NO min-height/padding of its own, inheriting .panel's
   100svh + vertical centring so the closing prompt gets a screen to itself. The button/note styles
   are shared with the hero (.btn-green / .cta-note), so the two CTAs can never drift. */
.closing-cta h2 { margin-bottom: 28px; }
/* Highlighter-pen swipe behind a run of text: a tilted, skewed stroke covering the letterforms,
   its ends tapered rather than cut so the ink reads as running dry at the start and end of a swipe.
   The tilt is a `transform` on a pseudo-element, NOT an angled background gradient — a rotated
   gradient's band drifts vertically in proportion to the box width, so the same rule would slant
   differently for every locale's string length (and run clean off a long one). */
.marker {
  /* The :root purple accent (#a371f7). */
  --marker-ink: rgba(163, 113, 247, 0.5);
  position: relative;
  display: inline-block;
  /* Keeps the stroke's negative z-index inside the span: behind its own text, above everything
     under it (without this it would sink below .content's starfield). */
  isolation: isolate;
}
.marker::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: -0.18em;
  right: -0.18em;
  top: 0.2em;
  bottom: -0.02em;
  background: linear-gradient(
    90deg,
    rgba(0, 0, 0, 0) 0%,
    var(--marker-ink) 3.5%,
    var(--marker-ink) 96.5%,
    rgba(0, 0, 0, 0) 100%
  );
  /* Dragged-pen geometry: the whole stroke leans, and the ends lean with it. */
  transform: rotate(-1.8deg) skewX(-11deg);
}
/* Narrow screens wrap the heading onto two lines, where one tilted box behind both reads as a
   mistake. Fall back to a per-line background swipe: `box-decoration-break: clone` redraws it on
   each line fragment, and the 100deg gradient tapers the ends to keep some of the pen feel. */
@media (max-width: 700px) {
  .marker {
    display: inline;
    background-image: linear-gradient(
      100deg,
      rgba(0, 0, 0, 0) 0.5%,
      var(--marker-ink) 2.5%,
      var(--marker-ink) 97.5%,
      rgba(0, 0, 0, 0) 99.5%
    );
    background-repeat: no-repeat;
    background-size: 100% 76%;
    background-position: 0 84%;
    padding: 0 0.18em;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }
  .marker::before { display: none; }
}

/* ===== Recent posts (#recent-posts) ===== */
/* The last band before the footer: a centred section heading over three blog cards — the SAME
   .blog-card markup and styles the blog index uses, so the two surfaces can never drift. Full
   landing width (1280px, like .hiw and .feature) and content-sized, since the full-screen .panel
   height would strand three short cards.
   The landing is locked dark and so misses the reading-theme tokens that `:is(.blog-body,
   .pricing-body)` grants (the :root accent is purple there, and --border-hover is unset, which would
   kill the card's hover border). Re-declaring the pair on the section gives the cards the blog's
   look; the light theme's higher-specificity :root[data-appearance='light'] rule still wins. */
.posts {
  min-height: 0;
  max-width: 1280px;
  padding-top: 0;
  padding-bottom: 120px;
  --accent: #3fb950;
  --border-hover: #59636e;
}
/* Heading left, "view all" right on one baseline-aligned row (the .hiw head's shape). */
.posts-head {
  width: 100%;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 28px;
  text-align: left;
}
/* Deliberately the smaller feature-heading size (#loop-engineering's), not the full .panel h2
   display size — this strip is a closing band, and a 48px heading over three small cards
   out-shouts the sections that carry the actual pitch. */
.posts h2 {
  font-size: clamp(24px, 2.4vw, 33px);
  margin: 0;
}
.posts-more {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  color: var(--muted);
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
  transition: color 0.15s ease;
}
.posts-more:hover { color: var(--text); }
.posts-more svg { transition: transform 0.15s ease; }
.posts-more:hover svg { transform: translateX(3px); }
@media (max-width: 600px) {
  .posts-head { flex-direction: column; align-items: flex-start; gap: 10px; }
}
/* An explicit three-up: .blog-grid's auto-fill would open a fourth track at this width and leave
   the three cards narrow beside an empty slot. */
.posts .blog-grid {
  grid-template-columns: repeat(3, 1fr);
  width: 100%;
  /* The section centres its heading; the cards inside read left-aligned. */
  text-align: left;
}
@media (max-width: 980px) {
  .posts .blog-grid { grid-template-columns: 1fr; }
}

/* ===== Footer scene (#footer) ===== */
/* Full-viewport art panel closing the page — fills the screen edge-to-edge
   like the hero background (same 1408×736 art frame, cover-cropped).
   Footer content will be layered on top of .footer-bg later. */
.site-footer {
  position: relative;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  /* Matches the content's bottom gradient stop (--sky-horizon). The video's
     top edge feathers into this (see .footer-bg-img mask), so the content sky
     hands off to the footer scene with no visible seam at any viewport width —
     the cover-crop moves which source row lands at the top, but the fade always
     resolves to this exact colour. */
  background: var(--sky-horizon);
  z-index: 2; /* rides above the sticky hero, like .content */
}
.footer-bg {
  position: absolute;
  inset: 0;
}
.footer-bg-img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* Dimmed so the scene reads as backdrop (footer-bar text stays legible). */
  opacity: 0.7;
  /* Feather the top edge so the video's sky melts into the sky-coloured footer
     background above it (which equals the content's bottom stop). */
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 64px);
  mask-image: linear-gradient(180deg, transparent 0, #000 64px);
}
/* Portrait phones: the cover crop is narrow, and a centred window slices the
   cave (the art's focal point, at ~2/3 width) in half at the screen edge —
   bias the crop so the cave stays in frame. */
@media (max-width: 600px) and (orientation: portrait) {
  .footer-bg-img { object-position: 67% center; }
}

/* ===== Back-to-top button ===== */
/* Fixed bottom-right on every public page. Hidden by default (and via the
   `hidden` attribute when JS is off); marketing.js adds .is-visible once the
   reader is a screenful in, and drops it before the footer video fills the
   screen (see update()). z-index sits below the fixed header (100). */
.back-to-top {
  position: fixed;
  right: 1.5rem;
  bottom: 1.5rem;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: rgba(20, 24, 33, 0.72);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s ease, background 0.2s ease;
  pointer-events: none;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.back-to-top[hidden] {
  display: none;
}
.back-to-top svg {
  width: 1.25rem;
  height: 1.25rem;
  display: block;
}
.back-to-top:hover {
  background: rgba(20, 24, 33, 0.9);
}
.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  .back-to-top {
    transition: opacity 0.25s ease, visibility 0.25s ease;
    transform: none;
  }
}
/* Narrow desktop/tablet: the es/pt nav labels run longer than English
   (Plataforma/Seguridad/Recursos/Conexiones + the language globe), so tighten
   the row until the mobile sheet takes over at 720px. */
@media (min-width: 721px) and (max-width: 900px) {
  .nav { gap: 8px; }
  .nav-links { gap: 0; margin-left: 0; }
  .nav-item { padding: 6px; font-size: 13px; }
  .nav-right { gap: 8px; }
}

@media (max-width: 720px) {
  /* GitHub-style mobile header: hamburger on the left (borderless, like
     github.com), logo centred in the bar, log-in button on the right.
     720px keeps tablets (iPad mini portrait is 768px, some report 764px)
     on the full desktop nav, which fits from ~720px up. */
  .nav-toggle {
    display: flex;
    order: -1;
    margin-left: 0;
    border: none;
    background: none;
  }
  .logo {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
  }

  /* Open menu = GitHub's mobile sheet: a white panel with rounded top
     corners filling the viewport below the (still dark) header, large bold
     dark items with right-pointing chevrons, green CTA block at the end. */
  .nav-links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0; right: 0;
    flex-direction: column;
    gap: 2px;
    margin: 0;
    padding: 16px 16px 24px;
    background: #fff;
    border-radius: 12px 12px 0 0;
    height: calc(100dvh - 100%);
    overflow-y: auto;
  }
  /* The animation re-fires on the display:none -> flex flip, giving the
     sheet a slide-down entrance without JS. */
  .nav-links.open { display: flex; animation: sheetIn 0.2s ease-out; }
  @keyframes sheetIn {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    .nav-links.open { animation: none; }
  }
  .nav-links li { display: block; width: 100%; }
  .nav-item {
    width: 100%;
    padding: 14px 8px;
    font-size: 20px;
    color: #1f2328;
  }
  .nav-item:hover { color: #1f2328; background: rgba(0, 0, 0, 0.04); }
  .has-caret { justify-content: space-between; }
  .has-caret::after {
    width: 8px; height: 8px;
    transform: rotate(-45deg);
    opacity: 0.5;
  }
  .has-dropdown.open .has-caret::after { transform: rotate(45deg); }

  /* Mega-menus become inline accordions on the white sheet (desktop mega is
     already light-themed — only layout needs overriding). */
  .mega {
    position: static;
    flex-direction: column;
    gap: 0;
    min-width: 0;
    border: none;
    box-shadow: none;
    background: transparent;
    padding: 0 0 6px 12px;
    display: none;
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .has-dropdown.open .mega { display: flex; }
  .mega-empty { white-space: normal; padding: 2px 8px 6px; }
  .mega-col { min-width: 0; gap: 0; }
  .mega-item small { display: none; }
  .mega-item:hover { background: rgba(0, 0, 0, 0.04); }

  /* Full-width CTA at the end of the sheet (GitHub's block button slot) */
  .nav-links .nav-cta { display: block; margin-top: 12px; }
  .nav-cta .btn-green {
    display: block;
    width: 100%;
    text-align: center;
    border-radius: 8px;
  }

  /* Language row on the sheet replaces the desktop globe dropdown. */
  .lang-switcher { display: none; }
  .nav-links .lang-mobile {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
    padding: 0 8px;
  }
  .lang-mobile .lang-option {
    padding: 8px 14px;
    border: 1px solid #d1d9e0;
    border-radius: 999px;
    font-size: 14px;
    font-weight: 600;
    color: #1f2328;
    text-decoration: none;
  }
  .lang-mobile .lang-option[aria-current="true"] {
    background: #1f2328;
    border-color: #1f2328;
    color: #fff;
  }
  /* Short single-word labels (config short_name) — keep each pill on one line. */
  .lang-mobile .lang-option { white-space: nowrap; }

  /* Top-bar auth: both buttons stay here (Sign up as text, Sign in bordered),
     just a tighter gap so the pair fits the mobile bar — the Start-for-free CTA
     still lives in the sheet. Smartphones drop "Sign up" (see the 600px block)
     so the lone bordered "Sign in" never crowds the centred logo. */
  .nav-right { gap: 8px; }

  /* Accordion submenu rows (Contact/Support/…) need finger-sized tap targets. */
  .mega-item { padding: 12px 8px; }
  .mega-text strong { font-size: 16px; }
}

/* ===== Small screens ===== */
@media (max-width: 600px) {
  /* Smartphone: the centred logo + hamburger leave room for one top-bar button,
     so drop "Sign up" and keep the bordered "Sign in". (Sign-up still reachable
     via the sheet's Start-for-free CTA.) */
  .nav-right .btn-text { display: none; }

  .hero-inner { --hero-lift: -2vh; }
  /* The desktop title break forces a one-word line at phone widths —
     let text-wrap: balance find the break instead. Same for the sub's
     sentence break, which strands "and more." on its own line. */
  .hero-title br,
  .hero-sub br { display: none; }
  /* Phones only: widen the torches so they stay chunky (tablets keep the
     base 9% — 15% of a tablet-wide frame reads huge). */
  .hero-torch { width: 15%; }
  .hero-sub {
    font-size: 16px;
    margin-bottom: 24px;
  }
  /* Keep the goblin at the very bottom, clear of the CTA */
  .goblin-center { bottom: 0; }
}

/* Ultra-narrow phones (Galaxy Z Fold cover screen, ~344px): the CTA pair
   can't sit side by side, so wrap it into a deliberate full-width stack
   instead of a ragged one, and ease the title size down. */
@media (max-width: 360px) {
  .hero-title { font-size: 32px; }
  .hero-cta { flex-direction: column; align-items: stretch; }
  .hero-cta .btn-green { text-align: center; }
  .hero-cta .btn-ghost { justify-content: center; }
}

/* Short viewports (small laptops): shrink the goblin so it never crowds the
   CTA block. The text keeps its normal position. */
@media (max-height: 760px) and (min-width: 601px) {
  .goblin { width: min(38vw, 240px); }
}

/* ===== Narrow (portrait-ish) screens ===== */
/* Plain cover-cropping scales the landscape background so far past the
   viewport that the torches at its sides disappear. Cap the overflow at
   130vw (torches live at 12–21% and 76–85% of the frame, so they stay in
   view), anchor the frame to the bottom where the path/crystals are, and
   fade its top edge into the hero background. The frame is stretched to the
   full hero height (the cave art tolerates the vertical stretch), with the
   torches widened to 15% to stay chunky on small screens. */
@media (max-aspect-ratio: 4/3) {
  .hero-bg-frame {
    top: auto;
    bottom: 0;
    height: 100%;
    width: max(100vw, min(100svh * (var(--img-w) / var(--img-h)), 130vw));
    transform: translateX(-50%);
  }
  .hero-bg-frame.bg-expanding { animation-name: bgExpandNarrow; }
  .hero-bg-img {
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 14%);
    mask-image: linear-gradient(to bottom, transparent 0, #000 14%);
  }
  .goblin { width: min(44vw, 240px); }
}
/* Portrait tablets (iPad, iPad Pro): the tall viewport strands the centred
   text high above the goblin with a dark hole between them, and the 9%
   torches get lost on the stretched frame. Sit the text lower and give the
   torches and goblin more presence. (Phones are excluded — they have their
   own lift/torch values in the max-width: 600px block — and so are 4:3
   LANDSCAPE tablets like the iPad mini at 1024×768, which hit the aspect
   branch exactly and got a 330px goblin towering over a 768px-tall hero.) */
@media (max-aspect-ratio: 4/3) and (min-width: 601px) and (orientation: portrait) {
  .hero-inner { --hero-lift: 2vh; }
  .hero-torch { width: 12.5%; }
  /* 40vw read huge on smaller tablets (iPad mini: 307px on a 1024px-tall
     screen) — 36vw/300px keeps the presence without crowding. */
  .goblin { width: min(36vw, 300px); }
}

/* Bottom-anchored variant of the CTA background zoom (no vertical centring
   translate to preserve). */
@keyframes bgExpandNarrow {
  /* Narrow layout doesn't apply the dolly-out zoom (see the frame's transform
     override in the media query), so this starts from 1×. */
  0%   { transform: translateX(-50%) scale(1); }
  100% { transform: translateX(-50%) scale(3.5); }
}

/* Portrait phones: the tall viewport leaves a dark hole between the CTA and a
   44vw goblin — give it more presence. (After the aspect-ratio blocks above so
   it wins their 44vw value on phones.) */
@media (max-width: 600px) and (max-aspect-ratio: 4/3) {
  .goblin { width: min(56vw, 260px); }
}

/* ===== Phone-portrait background art ===== */
/* Portrait phones swap to purpose-made 9:16 cave art (the <picture> source in
   index.html). It has the torches PAINTED IN, so the animated overlays are
   hidden, and the crystal clusters sit elsewhere, so sparkles use their
   --mx/--my coordinates. The frame drops the narrow-screen compensation hacks
   (130vw cap, stretch, top fade) — plain bottom-anchored cover fits real
   portrait art. Tablets (601px+) keep the desktop art + animated torches. */
@media (max-width: 600px) and (orientation: portrait) {
  .hero-bg-frame {
    --img-w: 720;
    --img-h: 1280;
    top: auto;
    bottom: 0;
    width: max(100vw, 100svh * (var(--img-w) / var(--img-h)));
    height: max(100svh, 100vw * (var(--img-h) / var(--img-w)));
    transform: translateX(-50%);
  }
  .hero-bg-img {
    -webkit-mask-image: none;
    mask-image: none;
  }
  .hero-torch { display: none; }
  .sparkle {
    left: var(--mx, var(--x));
    top: var(--my, var(--y));
  }
}

/* ===== Landscape phones (short viewports) ===== */
/* Phones on their side (~320–520px tall): the desktop metrics push the title
   under the fixed header and even a width-capped goblin fills half the screen.
   Compact the type, drop the forced line breaks (plenty of width here), reset
   the lift, and size the goblin by height instead of width. */
@media (max-height: 520px) and (orientation: landscape) {
  .hero-inner { --hero-lift: -3vh; }
  .hero-title {
    font-size: clamp(26px, 8vh, 34px);
    margin-bottom: 10px;
  }
  .hero-title br,
  .hero-sub br { display: none; }
  .hero-sub {
    font-size: 14px;
    max-width: 62ch;
    margin: 0 auto 16px;
  }
  .btn-green, .btn-ghost { padding: 13px 18px; font-size: 15px; }
  .cta-note { font-size: 12px; margin-top: 10px; }
  .goblin { width: auto; max-height: 30vh; }
  .goblin-center { bottom: 0; }
  /* No torches on landscape phones — they crowd the compacted hero text. */
  .hero-torch { display: none; }
}

/* ===== How it works — the loop carousel (#the-loop) ===== */
/* Text-only cards in a horizontal, scroll-snapped carousel (Daytona-style),
   nudged by the .hiw-arrow buttons in marketing.js; native swipe/scroll works
   without JS. Overrides the centred .panel defaults to a left-aligned header
   + full-width track. */
.hiw {
  max-width: 1280px;
  align-items: stretch;
  text-align: left;
}
.hiw-head {
  display: flex;
  align-items: last baseline;
  justify-content: space-between;
  gap: 24px;
  width: 100%;
}
.hiw-head-l { flex-shrink: 0; }
.hiw-head-l h2 { white-space: nowrap; margin-bottom: 0; }
.hiw-head-r {
  display: flex;
  align-items: last baseline;
  gap: 28px;
}
.hiw-sub {
  margin: 0;
  max-width: 42ch;
  text-align: left;
}
.hiw-arrows { display: flex; gap: 10px; flex-shrink: 0; }
.hiw-arrow {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: border-color 0.15s ease, background 0.15s ease, opacity 0.15s ease;
}
.hiw-arrow:hover { border-color: var(--muted); background: rgba(255, 255, 255, 0.05); }
.hiw-arrow:disabled { opacity: 0.35; cursor: default; }
.hiw-track-wrap { position: relative; width: 100%; margin-top: 44px; }
.hiw-track {
  display: flex;
  gap: 20px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding-bottom: 8px;
  scrollbar-width: none;
}
.hiw-track::-webkit-scrollbar { display: none; }
.hiw-track-wrap::after {
  content: "";
  position: absolute;
  top: 0;
  right: -1px;
  bottom: 8px;
  width: 80px;
  pointer-events: none;
  background: linear-gradient(to right, rgba(13, 17, 23, 0), var(--bg));
}
.hiw-card {
  scroll-snap-align: start;
  flex: 0 0 clamp(258px, 78vw, 340px);
  border: 1px solid var(--border);
  border-radius: 16px;
  /* Clip the full-bleed media tile to the card's rounded corners. */
  overflow: hidden;
  /* No bottom padding — the media tile sits flush against the card's bottom edge. */
  padding: 28px 26px 0;
  background: rgba(255, 255, 255, 0.03);
  display: flex;
  flex-direction: column;
  transition: background 0.15s ease;
}
/* Background only on hover — the border stays put (a colour shift there read as a jitter). */
.hiw-card:hover { background: rgba(255, 255, 255, 0.05); }
.hiw-card h3 {
  color: var(--text);
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
  margin: 0;
  text-wrap: balance;
}
.hiw-card p {
  color: var(--muted);
  font-size: 15px;
  line-height: 1.55;
  margin: 12px 0 0;
}
/* Placeholder illustration pinned to the card's bottom edge; margin-top:auto keeps every
   card's media aligned along one baseline as the flex row stretches cards to equal height. */
.hiw-card-media {
  margin-top: auto;
  padding-top: 24px;
  /* Bleed the tile out to the full card width (cancels the card's 26px side padding). */
  margin-inline: -26px;
}
.hiw-card-media img {
  display: block;
  width: 100%;
  height: auto;
}
@media (max-width: 720px) {
  .hiw-head { flex-direction: column; align-items: flex-start; gap: 20px; }
  .hiw-head-l h2 { white-space: normal; }
  .hiw-head-r { flex-direction: column; align-items: flex-start; gap: 16px; }
  .hiw-sub { max-width: none; }
  .hiw-arrows { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .hiw-track { scroll-behavior: auto; }
}

/* ===== Feature boxes (#code-reviews, siblings to come) ===== */
/* Cursor-home-style feature cards below the loop carousel: copy on the left,
   an interactive faux-browser on the right whose tab strip is real UI —
   marketing.js swaps the per-provider demo view on tab click (see
   plans/landing-feature-sections.md). Every colour derives from the
   --feature-* props so the one markup pattern renders correctly on BOTH
   themes: night values here, day values under the content pages'
   body.blog-body scope. New feature sections reuse these classes as-is. */
/* Content-sized section (NOT full-viewport like .panel/.hiw): it starts just
   after the loop carousel and is only as tall as its content + padding, so it
   doesn't reserve a whole screen. Widened and left-aligned like .hiw; no
   bordered card — the browser window IS the visual, sitting left with copy right. */
.feature {
  min-height: 0;
  max-width: 1280px;
  padding: 48px 24px 88px;
  text-align: left;
  align-items: stretch;
  --feature-text: var(--text);
  --feature-muted: var(--muted);
  --feature-chrome: #10151d;
  --feature-border: var(--border);
  --feature-tab-active: rgba(255, 255, 255, 0.08);
  --feature-tab-hover: rgba(255, 255, 255, 0.05);
  --feature-screen: #0a0d12;
  --feature-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
}
body.blog-body .feature {
  --feature-text: #1f2328;
  --feature-muted: #59636e;
  --feature-chrome: #eaeef2;
  --feature-border: #d1d9e0;
  --feature-tab-active: #fff;
  --feature-tab-hover: rgba(31, 35, 40, 0.06);
  --feature-screen: #fff;
  --feature-shadow: 0 30px 60px rgba(31, 35, 40, 0.14);
}
.feature-card {
  display: grid;
  grid-template-columns: 3fr 2fr;
  gap: clamp(25px, 5vw, 32px);
  /* Top-align both columns so the copy starts level with the browser frame's
     top edge (rather than centering against its taller height). */
  align-items: start;
  width: 100%;
}
/* Layout follows DOM order: the default section is browser-first (browser wide
   on the left, copy right). The reversed section (#tasks) is copy-first and
   widens the right column, so copy sits left with the browser wide on the right. */
.feature-reverse .feature-card { grid-template-columns: 2fr 3fr; }
.feature-copy h2 {
  color: var(--feature-text);
  font-size: clamp(28px, 3vw, 42px);
  text-align: left;
  margin: 0;
}
/* Keep a titled phrase (e.g. "reviewed Automagically") from wrapping mid-line. */
.feature-nowrap { white-space: nowrap; }
/* The loop-engineering headline is three full sentences (<br />-separated) — cap it
   well below the default clamp so each sentence holds a single line on large screens. */
#loop-engineering .feature-copy h2 { font-size: clamp(24px, 2.4vw, 33px); }
.feature-desc {
  color: var(--feature-muted);
  font-size: 17px;
  line-height: 1.6;
  max-width: 46ch;
  margin: 20px 0 0;
}
/* The faux browser window: chrome bar (traffic lights + Safari-style centered
   tab pills) over the demo view. Swap a view's .video-ph for a real
   <video class="browser-video"> once its screencast is recorded. */
.feature-browser {
  border: 1px solid var(--feature-border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--feature-screen);
  box-shadow: var(--feature-shadow);
}
.browser-chrome {
  background: var(--feature-chrome);
  border-bottom: 1px solid var(--feature-border);
}
/* Row 1: [ traffic lights + nav ] [ centered address pill ] [ action icons ].
   The 1fr auto 1fr grid keeps the pill centered whatever the side widths. */
.browser-bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  padding: 9px 14px;
}
.browser-dots {
  flex: none;
  width: 44px;
  height: 11px;
  background:
    radial-gradient(circle 5px at 5px 6px, #ff5f57 98%, transparent 100%),
    radial-gradient(circle 5px at 22px 6px, #febc2e 98%, transparent 100%),
    radial-gradient(circle 5px at 39px 6px, #28c840 98%, transparent 100%);
}
/* The address pill contrasts against the chrome (screen-coloured, like a real
   URL field). */
.browser-url {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  justify-self: center;
  width: clamp(180px, 30vw, 320px);
  padding: 5px 14px;
  border-radius: 8px;
  background: var(--feature-screen);
  border: 1px solid var(--feature-border);
  color: var(--feature-muted);
  font-size: 13px;
}
.browser-lock { width: 11px; height: 11px; flex: none; opacity: 0.7; }
.browser-url-host { color: var(--feature-text); font-weight: 500; }
/* Row 2: full-width tabs split evenly; the active tab takes the view's own
   background so it reads as the foreground page. */
.browser-tabs {
  display: flex;
  border-top: 1px solid var(--feature-border);
  /* Several tabs (the issue-tracker section has four) scroll rather than crush. */
  overflow-x: auto;
  scrollbar-width: none;
}
.browser-tabs::-webkit-scrollbar { display: none; }
.browser-tab {
  flex: 1;
  min-width: max-content;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--feature-muted);
  background: transparent;
  border: none;
  border-right: 1px solid var(--feature-border);
  padding: 9px 14px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.browser-tab:last-child { border-right: none; }
.browser-tab:hover { color: var(--feature-text); background: var(--feature-tab-hover); }
.browser-tab.active {
  color: var(--feature-text);
  background: var(--feature-screen);
}
/* Narrow screens: let the address pill breathe. */
@media (max-width: 520px) {
  .browser-url { width: auto; min-width: 0; }
}
/* Views swap with no fade — animating the
   large screen area reads as a blink. */
.browser-view { background: var(--feature-screen); }
.browser-view[hidden] { display: none; }
/* Faux-browser demo placeholder — shared by every feature section, shown until a
   screencast is recorded (swap .video-ph for <video class="browser-video">).
   .int-ico sizes the tab icons. All theme-aware via --feature-* + the brand green. */
.int-ico { width: 15px; height: 15px; flex: none; }
.video-ph {
  aspect-ratio: 4 / 3;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(46, 160, 67, 0.09), transparent 60%),
    var(--feature-screen);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
}
.video-play {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 40px rgba(46, 160, 67, 0.35);
}
.video-play::after {
  content: "";
  position: absolute;
  left: 55%;
  top: 50%;
  transform: translate(-50%, -50%);
  border-left: 18px solid #fff;
  border-top: 11px solid transparent;
  border-bottom: 11px solid transparent;
}
.video-note { color: var(--feature-muted); font-size: 14px; }
/* The real recording fills the window body edge-to-edge. */
.browser-video {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--feature-screen);
}
@media (max-width: 900px) {
  .feature-card,
  .feature-reverse .feature-card {
    grid-template-columns: 1fr;
    gap: 28px;
  }
  .feature-desc { max-width: none; }
}
