/* Grove marketing site — tokens and rules from references/design-brief.md.
   Monochrome + one accent, Inter body / Instrument Sans display, strict 4px
   grid (1px hairlines exempt as drawing details). */

:root {
  --bg: #ffffff;
  --bg-tint: #f5f5f4;
  --bg-hover: #ededec;
  /* Matches the app's own --bg-active (src/tokens.css) — the hero demo's
     selected sidebar row/tab needs a third tint one step past --bg-hover. */
  --bg-active: #e4e4e3;
  --text: #1a1a1a;
  --text-secondary: #6b6b6b;
  --text-faint: #a3a3a3;
  --border: #e2e2e1;
  /* Brand green, refreshed from the Figma hero button's #20a241 — kept its
     hue/saturation (H135 S67) but pulled its lightness down ~10 points
     (38% → 28%) so it grounds against the page instead of popping too
     bright/light, while still reading with far more contrast than the
     old, more muted #3e7a57 it replaces. Originally kept independent of
     the app's own --accent, but the app was brought in line with this
     value on `ui-polish` (2026-08-17) — both now match intentionally;
     see src/tokens.css. */
  --accent: #187730;
  --accent-bg: #18773014;
  /* This light-mode green specifically, kept as a fixed (non-themed)
     value so the hero waitlist button can reuse it even in dark mode —
     at Nikhil's request, it reads sharper there than dark mode's own
     lighter --accent. Never redefined in the dark blocks below, unlike
     --accent itself. See .hero .waitlist button. */
  --accent-light: #187730;
  /* Website-only tonal range, still derived from --accent — never used in the app itself. */
  --accent-deep: #1f4530;
  --accent-tint: #eaf3ee;
  --on-accent-deep: #f5f5f4;
  --float-shadow: 0 12px 32px rgb(20 32 26 / 0.14), 0 2px 6px rgb(20 32 26 / 0.07);
  /* Text sitting on top of the hero backdrop photograph (see "Hero
     backdrop" below). Deliberately NOT in the light/dark token blocks:
     the backdrop is the same dark image in both themes, so anything drawn
     over it has to stay light regardless of the visitor's theme —
     --text would go near-black in light mode and disappear. */
  --on-backdrop: #e8e8e8;
}

/* Dark tokens apply via system preference (no-JS fallback) unless the user
   explicitly picked light; the [data-theme="dark"] block covers the explicit
   pick and must stay in sync with the media-query block. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #1e1e1e;
    --bg-tint: #262626;
    --bg-hover: #2e2e2e;
    --bg-active: #363636;
    --text: #e8e8e8;
    --text-secondary: #9a9a9a;
    --text-faint: #6b6b6b;
    --border: #333333;
    /* Same H135/S67 family as light mode's --accent, lightened and
       de-saturated a step (28%→55% L, 67%→45% S) for dark-mode contrast
       without reading neon against near-black. */
    --accent: #59c073;
    --accent-bg: #59c0731f;
    --accent-deep: #0e1f16;
    --accent-tint: #16221c;
    --on-accent-deep: #eaf3ee;
    --float-shadow: 0 12px 32px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.35);
  }
}

:root[data-theme="dark"] {
  --bg: #1e1e1e;
  --bg-tint: #262626;
  --bg-hover: #2e2e2e;
  --bg-active: #363636;
  --text: #e8e8e8;
  --text-secondary: #9a9a9a;
  --text-faint: #6b6b6b;
  --border: #333333;
  --accent: #59c073;
  --accent-bg: #59c0731f;
  --accent-deep: #0e1f16;
  --accent-tint: #16221c;
  --on-accent-deep: #eaf3ee;
  --float-shadow: 0 12px 32px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.35);
}

/* Instrument Sans (display, H1/H2) and Inter (body, everywhere else) —
   both SIL OFL, self-hosted (no Google Fonts CDN request at runtime), single
   variable-weight files covering 400–700. The two format hints let
   non-variable-font browsers skip straight to the plain woff2 src. */
@font-face {
  font-family: "Instrument Sans";
  src:
    url("assets/fonts/InstrumentSans-Variable.woff2") format("woff2-variations"),
    url("assets/fonts/InstrumentSans-Variable.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src:
    url("assets/fonts/Inter-Variable.woff2") format("woff2-variations"),
    url("assets/fonts/Inter-Variable.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

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

/* Scrollbar: thin themed thumb over a faint track tint. */
html {
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--text-faint) 70%, transparent) var(--bg-tint);
}

::-webkit-scrollbar {
  width: 10px;
}

::-webkit-scrollbar-track {
  background: var(--bg-tint);
}

::-webkit-scrollbar-thumb {
  background-color: color-mix(in srgb, var(--text-faint) 70%, transparent);
  border-radius: 8px;
  border: 2px solid transparent;
  background-clip: padding-box;
}

html {
  scroll-behavior: smooth;
}

body {
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 24px;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

code {
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  background: var(--bg-tint);
  padding: 0 4px;
  border-radius: 4px;
}

::selection {
  background: var(--accent-bg);
}

/* ---------- Nav ---------- */

/* Edge-to-edge hairline bar at rest; scrolling past the top (see the
   .is-floating toggle in index.html/changelog.html) morphs it into an
   inset, rounded, floating pill and back — same ease as the rest of the
   site's motion (.reveal, the hero demo's pane rise). The stuck offset is
   driven by `top`, not margin-top: sticky elements pin their border box to
   `top`'s value regardless of margin, so margin-top alone leaves the bar
   flush against the viewport edge instead of gapped. Border stays 1px on
   every side throughout so only its color animates, rather than widths
   appearing/disappearing out of nowhere. */
.nav {
  position: sticky;
  top: 0;
  margin: 0 auto;
  /* A large fixed value rather than none/100% — max-width can't interpolate
     from the none keyword, so the floating state's narrower max-width would
     snap in instantly instead of easing like everything else. 1600px still
     left the border-bottom stroke short of the viewport edge on wide
     screens (confirmed at 2000px); 4800px comfortably exceeds any real
     desktop viewport's CSS pixel width today. */
  max-width: 4800px;
  background-color: color-mix(in srgb, var(--bg) 70%, transparent);
  /* saturate() alongside blur is what reads as frosted glass rather than
     plain gaussian blur — same recipe as .app-window's glass mat below. */
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  border: 1px solid transparent;
  border-bottom-color: var(--border);
  border-radius: 0;
  box-shadow: none;
  z-index: 10;
  transition:
    top 400ms cubic-bezier(0.2, 0.7, 0.3, 1),
    max-width 400ms cubic-bezier(0.2, 0.7, 0.3, 1),
    border-radius 400ms cubic-bezier(0.2, 0.7, 0.3, 1),
    border-color 400ms cubic-bezier(0.2, 0.7, 0.3, 1),
    box-shadow 400ms cubic-bezier(0.2, 0.7, 0.3, 1),
    background-color 400ms cubic-bezier(0.2, 0.7, 0.3, 1),
    backdrop-filter 400ms cubic-bezier(0.2, 0.7, 0.3, 1);
}

.nav.is-floating {
  top: 24px;
  /* 1104px matches the hero app demo's own rendered width exactly (main's
     1152px max-width less its 24px side padding), so the floating pill's
     edges line up with the demo window's edges directly below it rather
     than sitting on their own narrower measure.
     min(), not a bare 1104px — below ~1136px wide the pill would exceed the
     viewport, so margin:auto has no room left to create side gaps and the
     "floating" pill touches both edges instead of insetting (the bug this
     fixes). The fallback is calc(100% - 48px), not the 32px it used to be,
     because 48px is exactly main's own side padding (24px x 2) — that keeps
     the pill flush with the demo below it at narrow widths too, instead of
     matching only above the crossover and then sitting 8px wider per side. */
  max-width: min(1104px, calc(100% - 48px));
  background-color: color-mix(in srgb, var(--bg) 62%, transparent);
  backdrop-filter: blur(24px) saturate(1.6);
  -webkit-backdrop-filter: blur(24px) saturate(1.6);
  border-color: color-mix(in srgb, var(--border) 60%, transparent);
  border-radius: 999px;
  /* A soft top catch-light on top of the float lift — same "light hitting
     the glass" cue as .app-window's inset highlight, kept neutral (mixed
     with white, not the accent) so the nav itself stays chrome, not themed. */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, white 25%, transparent),
    var(--float-shadow);
}

.nav-inner {
  position: relative;
  max-width: 1152px;
  margin: 0 auto;
  padding: 12px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Floating pill needs less side padding than the edge-to-edge bar so the
   waitlist button reads as fully enclosed within the rounded ends, not
   floating loose inside oversized margins. */
.nav.is-floating .nav-inner {
  padding-left: 24px;
  padding-right: 16px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: var(--text);
  font-weight: 600;
  font-size: 16px;
}

.brand img {
  border-radius: 4px;
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Centered on .nav-inner itself, independent of the brand mark's and the
   toggle+CTA cluster's widths — matches the Figma nav, where the links sit
   in the header's true center rather than beside the right-hand controls. */
.nav-links {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 32px;
}

.nav-links a {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  text-decoration: none;
}

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

.nav-links a[aria-current="page"] {
  color: var(--text);
}

.theme-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: none;
  border: none;
  border-radius: 8px;
  color: var(--text-secondary);
  cursor: pointer;
}

.theme-toggle:hover {
  background: var(--bg-hover);
  color: var(--text);
}

.theme-toggle .icon-sun {
  display: none;
}

[data-theme="dark"] .theme-toggle .icon-sun {
  display: block;
}

[data-theme="dark"] .theme-toggle .icon-moon {
  display: none;
}

/* Neutral, not accent-filled: the hero's own waitlist button is the page's
   one strong accent moment, and over the hero backdrop photograph (below)
   a green pill on green foliage all but disappears. --text/--bg give the
   Figma's white-on-#1e1e1e in dark mode and invert to a dark pill on the
   light doc pages, high-contrast either way. */
.nav-cta {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--bg);
  text-decoration: none;
  background: var(--text);
  padding: 8px 20px;
  border: none;
  border-radius: 999px;
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.25),
    inset 0 -1px 3px rgb(0 0 0 / 0.15);
  transition: box-shadow 0.2s ease, background-color 0.2s ease;
}

/* Mixes toward --bg rather than brightness() — brightening an already
   near-white pill produces no visible hover at all. */
.nav-cta:hover {
  background: color-mix(in srgb, var(--text) 88%, var(--bg));
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.3),
    inset 0 -1px 3px rgb(0 0 0 / 0.15);
}

/* Once the nav is floating, it sits on its own frosted pill rather than
   directly on the backdrop photo — the neutral --text/--bg pill's
   contrast rationale above no longer applies, so brand green reads fine.
   Light mode only, at Nikhil's request: dark mode's floating pill keeps
   the neutral look, reasserted below (same selector inside the dark
   blocks, which come later in source order so they win the tie). */
.nav.is-floating .nav-cta {
  background: var(--accent);
  color: #ffffff;
}

.nav.is-floating .nav-cta:hover {
  background: color-mix(in srgb, var(--accent) 85%, black);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .nav.is-floating .nav-cta {
    background: var(--text);
    color: var(--bg);
  }

  :root:not([data-theme="light"]) .nav.is-floating .nav-cta:hover {
    background: color-mix(in srgb, var(--text) 88%, var(--bg));
  }
}

[data-theme="dark"] .nav.is-floating .nav-cta {
  background: var(--text);
  color: var(--bg);
}

[data-theme="dark"] .nav.is-floating .nav-cta:hover {
  background: color-mix(in srgb, var(--text) 88%, var(--bg));
}

/* ---------- Nav over the hero backdrop ----------
   On the homepage the nav sits on top of the backdrop photograph, so at
   rest it draws no chrome of its own — no fill, no blur, no hairline —
   and the image runs unbroken to the very top of the page (Figma's header,
   node 18:5323, carries no fill). The moment it floats it goes back to the
   shared frosted-pill treatment above, which reads fine over the image.
   Scoped to .is-over-hero, set only on index.html's header: the doc pages
   share this stylesheet but have no backdrop to sit on. */
.nav.is-over-hero:not(.is-floating) {
  background-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
}

/* Pinned to the backdrop's light value in both themes, same reasoning as
   --on-backdrop itself. --text-secondary in particular is a mid grey that
   barely separates from the photograph. */
.nav.is-over-hero:not(.is-floating) .brand,
.nav.is-over-hero:not(.is-floating) .nav-links a,
.nav.is-over-hero:not(.is-floating) .theme-toggle {
  color: var(--on-backdrop);
}

.nav.is-over-hero:not(.is-floating) .theme-toggle:hover {
  background: rgb(255 255 255 / 0.14);
  color: #ffffff;
}

.nav.is-over-hero:not(.is-floating) .nav-cta {
  background: #ffffff;
  color: #1e1e1e;
}

.nav.is-over-hero:not(.is-floating) .nav-cta:hover {
  background: #ececec;
}

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

main {
  max-width: 1152px;
  margin: 0 auto;
  padding: 0 24px;
  /* Decorative layers inside the column (the app demo's two glow pseudos)
     are sized to bleed past their own box, which is fine while the 1152px
     column has room to spare but pushes past the viewport once the column
     *is* the viewport — below ~944px that made the whole page scroll
     sideways. `clip`, not `hidden`: it stops the overflow without turning
     main into a scroll container, and it pairs with `overflow-y: visible`
     so nothing changes vertically. The one sticky element on the page,
     .nav, sits outside main and is unaffected. */
  overflow-x: clip;
}

section {
  padding: 96px 0;
}

h2 {
  font-family: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 36px;
  line-height: 44px;
  font-weight: 600;
  margin-bottom: 32px;
}

h3 {
  font-size: 16px;
  line-height: 24px;
  font-weight: 600;
  margin-bottom: 8px;
}

/* ---------- Hero backdrop ----------
   A full-bleed photograph behind the nav, the hero copy, and the top ~60%
   of the app demo, then a hard edge back to --bg — the Figma "bg" frame
   (node 18:4832): a 1442x946 object-cover image anchored to the very top
   of the page, deliberately stopping partway down the demo window rather
   than running behind all of it. A fade-to-transparent (mask-image) plus
   a blur was tried here to soften that edge into a dissolve; both were
   reverted — the hard edge read better.

   Painted from body::before rather than .hero::before for two reasons:
   .hero lives inside main's 1152px column, so escaping it would need
   100vw — which overshoots by the scrollbar's width on this site (the
   scrollbar is always visible and gutter-stable) and introduces
   horizontal overflow; and the layer has to start above .hero's own box
   to cover the nav. An absolutely positioned body::before resolves
   against the initial containing block, giving exactly the viewport
   width at the document origin.

   z-index: -1 sits it above the page background (body's `background`
   propagates to the canvas, which paints behind negative-z-index boxes)
   and below every in-flow element. The app demo's own glow pseudos are
   also z-index: -1 in this same root stacking context; they come later in
   tree order, so they still layer on top of this. */
/* Tuned against the live page, not lifted from Figma's 946px: the mock's
   demo window is taller than the real component, so the same absolute
   height would land much further down it. 982px puts the edge ~65% of the
   way down the (now-widened, taller) window, matching the mock's
   proportion. Retuned alongside the app-demo width/height increase —
   see .app-demo's own comment for that ratio. */
.home {
  --hero-backdrop-h: 982px;
}

.home::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--hero-backdrop-h);
  z-index: -1;
  background-image: url("assets/hero-backdrop.jpg");
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  pointer-events: none;
}

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

.hero {
  padding-top: 96px;
  text-align: center;
}

/* A bordered pill rather than plain accent text — states a fact (the live
   waitlist count) instead of drawing attention like a status label would.
   Hidden by default in the markup; the hero script reveals it once a real
   count has loaded, so it never shows a stale or wrong number. */
.eyebrow {
  display: inline-block;
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 12px;
  margin-bottom: 16px;
}

/* The rule above sets an explicit display, which outranks the UA stylesheet's
   own [hidden] { display: none } — so the markup's `hidden` attribute alone
   did not actually hide the pill, and a failed count fetch left an empty
   bordered pill with a lone pulsing dot sitting in the hero. Any styled
   element that relies on `hidden` needs this restated. */
.eyebrow[hidden] {
  display: none;
}

/* Sits on the hero's photo backdrop, same reasoning as --on-backdrop on the
   H1/lede below it: --text-secondary and --border barely separate from the
   photograph. */
.hero .eyebrow {
  color: var(--on-backdrop);
  border-color: rgb(255 255 255 / 0.28);
}

/* Green (--accent-light, same as the waitlist button) kept losing to the
   photo, which is itself mostly green — a dark ring and then a white stroke
   around the green fill both still fought the backdrop. Pinned to
   --on-backdrop instead: the same fixed light color already guaranteed
   legible for the H1/lede/nav sitting on this exact photo, so the dot no
   longer needs its own contrast trick. Trade-off: it reads as a plain
   status dot now, not a "green = live" cue. */
.eyebrow-dot {
  position: relative;
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 6px;
  vertical-align: middle;
  border-radius: 50%;
  background: var(--on-backdrop);
}

.eyebrow-dot::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: var(--on-backdrop);
  animation: eyebrow-dot-pulse 2s ease-in-out infinite;
}

@keyframes eyebrow-dot-pulse {
  0% {
    opacity: 0.55;
    transform: scale(0.5);
  }
  70%, 100% {
    opacity: 0;
    transform: scale(1.6);
  }
}

/* The container is wide enough for the bento grid, so hero copy needs its own
   measure — otherwise the headline runs to a single overlong line.
   Direct-child selector (> h1), not a plain descendant one — the app demo
   mockup nested inside .hero has its own <h1> ("Pokedex", styled by
   .md-body h1 with --text), and the old .hero h1 descendant selector
   matched that too, painting it with --on-backdrop's light grey meant
   only for text sitting directly on the backdrop photo. */
.hero > h1 {
  font-family: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 64px;
  line-height: 64px;
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--on-backdrop);
  max-width: 720px;
  margin: 0 auto 24px;
  text-wrap: balance;
}

/* Smaller than the headline's old 20px counterpart: with the H1 up at
   64px the lede's job is supporting detail, not a second headline. */
.lede {
  font-size: 18px;
  line-height: 27px;
  color: var(--on-backdrop);
  max-width: 560px;
  margin: 0 auto 32px;
}

/* ---------- Waitlist form ---------- */

.waitlist {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  max-width: 440px;
  margin: 0 auto;
}

.waitlist button {
  font: inherit;
  font-weight: 600;
  color: var(--bg);
  background: var(--accent);
  border: none;
  border-radius: 999px;
  padding: 8px 24px;
  height: 48px;
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.25),
    inset 0 -2px 4px rgb(0 0 0 / 0.15),
    0 3px 10px color-mix(in srgb, var(--accent) 25%, transparent),
    0 0 16px color-mix(in srgb, var(--accent) 12%, transparent);
  transition: box-shadow 0.2s ease, filter 0.2s ease;
}

.waitlist button:hover {
  filter: brightness(1.08);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.3),
    inset 0 -2px 4px rgb(0 0 0 / 0.15),
    0 4px 14px color-mix(in srgb, var(--accent) 32%, transparent),
    0 0 20px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* The hero button alone pins to --accent-light (the fixed light-mode
   green) and literal white text instead of the shared rule's themed
   --accent/--bg pair — in dark mode, --accent's own lighter, less
   saturated green read softer here than Nikhil wanted; light mode looks
   identical either way since --accent already equals --accent-light and
   --bg is already white there. The footer CTA button keeps the themed
   --accent, unaffected. */
.hero .waitlist button {
  color: #ffffff;
  background: var(--accent-light);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.25),
    inset 0 -2px 4px rgb(0 0 0 / 0.15),
    0 3px 10px color-mix(in srgb, var(--accent-light) 25%, transparent),
    0 0 16px color-mix(in srgb, var(--accent-light) 12%, transparent);
}

.hero .waitlist button:hover {
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.3),
    inset 0 -2px 4px rgb(0 0 0 / 0.15),
    0 4px 14px color-mix(in srgb, var(--accent-light) 32%, transparent),
    0 0 20px color-mix(in srgb, var(--accent-light) 18%, transparent);
}

/* ---------- Hero app demo ----------
   A real HTML/CSS rendering of Grove's window (sidebar, tabs, breadcrumbs,
   three document panes) that auto-cycles in place of a screenshot, so it
   never goes stale the way a PNG does. Ported whole from a standalone
   Artifact build — every value already came from src/tokens.css and this
   file's own tokens (see references/flow-website.md). Icon ids are
   prefixed hi- (hero icon) so they can't collide with the bento sprite's
   bi- ids. Driven by the <script> at the bottom of index.html, which
   toggles data-view on .app-demo and is hover-pause / reduced-motion aware. */

/* Stretched to fill the hero's full ~1104px content column (main's
   max-width minus its own side padding — the same content width nav-inner
   resolves to), rather than the original 880px design width, so the demo
   reads as the page's centerpiece instead of a smaller object floating in
   a wide column. --sidebar-w and --frame-h are scaled up from their
   original 880px-tuned values by the same ratio (1104/880 ≈ 1.2545) so the
   window's proportions — sidebar-to-frame ratio, frame height-to-width —
   stay exactly as designed, just larger. max-width is set past 1104px on
   purpose: main is always the binding constraint at every viewport this
   rule applies at, so the demo simply inherits main's actual width rather
   than needing its own breakpoint-matched value. */
.app-demo {
  --sidebar-w: 271px;
  --frame-h: 652px;
  position: relative;
  max-width: 1152px;
  margin: 64px auto 0;
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  text-align: left;
}

/* Frosted glass mat: .app-window is the translucent, blurred outer layer
   (padding gives it visible thickness so the blur actually shows — with
   zero padding there'd be no exposed surface for backdrop-filter to
   render, since the opaque sidebar/main fill the box edge-to-edge).
   .app-frame is an inset, fully opaque floating card (its own radius
   and shadow, same recipe as the bento cards' floating UI) so the
   demo's text stays crisp while the glass ring around it stays
   translucent. box-shadow is the same plain --float-shadow every other
   floating card on the page uses — the earlier accent-green catch-light
   line, top wash, ambient outer glow, and the separate ::before/::after
   glow-orb pseudos behind the window were all removed; the frosted mat
   now reads as a neutral glass card, not a colored light source. */
.app-window {
  padding: 14px;
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: 20px;
  /* 55%, not the 78% this used to be: at 78% the near-black --bg swallowed
     the backdrop photo almost entirely, so the mat read as a plain dark rim
     and the blur behind it was invisible. The glass only reads as glass when
     enough of the blurred photo survives through the fill. */
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(24px) saturate(1.6);
  -webkit-backdrop-filter: blur(24px) saturate(1.6);
  /* Neutral top catch-light (mixed with white, never the accent — the old
     accent-tinted glow treatment stays removed), same cue the floating nav
     pill uses so the two glass surfaces on the page read as one material. */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, white 25%, transparent),
    var(--float-shadow);
}

.app-frame {
  display: flex;
  height: var(--frame-h);
  background: var(--bg);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: var(--float-shadow);
}

/* ------------------------------------------------------------ sidebar ---- */

.app-sidebar {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  width: var(--sidebar-w);
  background: var(--bg-tint);
  border-right: 1px solid var(--border);
}

/* The sidebar tint extends into the title-bar region, as it does in the app. */
.sb-lights {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 36px;
  padding: 0 16px;
  flex-shrink: 0;
}

.sb-lights i {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--border);
}

.sb-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px 8px 16px;
  flex-shrink: 0;
}

.sb-vault {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-secondary);
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
}

.sb-head span.ico {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 4px;
  color: var(--text-secondary);
}

.sb-tree {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  padding: 0 8px;
}

.sb-row {
  display: flex;
  align-items: center;
  gap: 4px;
  height: 28px;
  padding-right: 4px;
  border-radius: 4px;
  color: var(--text-secondary);
  font-size: 12px;
  line-height: 16px;
  transition: background-color 200ms ease, color 200ms ease;
}

.sb-row[data-file] { cursor: pointer; }
.sb-row[data-file]:hover { background: var(--bg-tint); color: var(--text); }
.sb-row[data-file]:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }

.sb-row.root { color: var(--text); font-weight: 600; }
.sb-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sb-row svg { flex-shrink: 0; }
.sb-row .chev { color: var(--text-faint); }

/* Dev is a header row, not a real indent level — its direct children (depth 1)
   sit flush under its own icon rather than stepping in a further 16px, then
   nest normally from there. Each further depth still steps by 16px, matching
   FileTree.tsx's real formula, just re-based so the ladder starts at Dev. */
.sb-row[data-depth="0"] { padding-left: 8px; }
.sb-row[data-depth="1"] { padding-left: 8px; }
.sb-row[data-depth="2"] { padding-left: 24px; }

/* A file row has no chevron, so its icon would sit 20px (chevron + gap) left
   of a sibling folder's icon at the same depth. The real app compensates with
   the same marginLeft:20 on FileIcon — reproduced here rather than guessed. */
.leaf-ico { margin-left: 20px; }

/* Selection follows the pane on show. */
.app-demo[data-view="md"] .sb-row[data-file="md"],
.app-demo[data-view="json"] .sb-row[data-file="json"],
.app-demo[data-view="canvas"] .sb-row[data-file="canvas"] {
  color: var(--text);
  background: var(--bg-active);
  font-weight: 500;
}

.sb-footer {
  flex-shrink: 0;
  padding: 4px 8px 8px;
  border-top: 1px solid var(--border);
}

.sb-settings {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  height: 28px;
  padding: 0 8px;
  border-radius: 4px;
  color: var(--text-secondary);
  font-size: 12px;
  line-height: 16px;
}

.sb-settings .hint { margin-left: auto; color: var(--text-faint); }

/* --------------------------------------------------------------- main ---- */

.app-main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

.app-tabs {
  display: flex;
  align-items: stretch;
  height: 36px;
  flex-shrink: 0;
  border-bottom: 1px solid var(--border);
}

.tab-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  flex-shrink: 0;
  color: var(--text-secondary);
  border-right: 1px solid var(--border);
}

.tab {
  position: relative;
  display: flex;
  align-items: center;
  gap: 4px;
  height: 36px;
  margin-bottom: -1px;
  padding: 0 12px;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid transparent;
  color: var(--text-secondary);
  font-size: 12px;
  line-height: 16px;
  transition: color 200ms ease, background-color 200ms ease;
}

.tab .label {
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tab .close { display: flex; color: var(--text-faint); }

.tab[data-file] { cursor: pointer; }
.tab[data-file]:hover { color: var(--text); }
.tab[data-file]:focus-visible { outline: 1px solid var(--accent); outline-offset: -2px; }

.app-demo[data-view="md"] .tab[data-file="md"],
.app-demo[data-view="json"] .tab[data-file="json"],
.app-demo[data-view="canvas"] .tab[data-file="canvas"] {
  color: var(--text);
  font-weight: 500;
  background: var(--bg);
  border-bottom-color: var(--bg);
}

/* ------------------------------------------------------------- header ---- */

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  min-height: 40px;
  flex-shrink: 0;
  padding: 4px 12px 4px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

.crumbs {
  position: relative;
  flex: 1;
  min-width: 0;
  height: 24px;
}

.crumb-set {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 2px;
  overflow: hidden;
  color: var(--text-secondary);
  font-size: 12px;
  line-height: 16px;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 200ms ease, transform 200ms ease;
  pointer-events: none;
}

.crumb-set span { padding: 4px; white-space: nowrap; }
.crumb-set .current { color: var(--text); font-weight: 500; }
.crumb-set svg { flex-shrink: 0; color: var(--text-faint); }

.app-demo[data-view="md"] .crumb-set[data-file="md"],
.app-demo[data-view="json"] .crumb-set[data-file="json"],
.app-demo[data-view="canvas"] .crumb-set[data-file="canvas"] {
  opacity: 1;
  transform: none;
}

.header-actions {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 4px;
}

.view-toggle {
  position: relative;
  display: flex;
  align-items: center;
  gap: 4px;
  height: 32px;
  padding: 4px;
  border-radius: 4px;
  background: var(--bg-tint);
}

.view-toggle button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: 0;
  padding: 0;
  border-radius: 4px;
  color: var(--text-secondary);
  background: transparent;
  font: inherit;
  cursor: pointer;
  transition: color 200ms ease, background-color 200ms ease;
}

.view-toggle button:hover { color: var(--text); }
.view-toggle button.on { color: var(--text); background: var(--bg-active); }
.view-toggle button:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

.more-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 4px;
  color: var(--text-secondary);
}

/* The canvas has no preview/source toggle in the app, so it hides here too. */
.app-demo[data-view="canvas"] .view-toggle { display: none; }

/* --------------------------------------------------------------- panes --- */

.pane-stack {
  position: relative;
  flex: 1;
  min-height: 0;
  background: var(--bg);
}

.app-pane {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 320ms ease, transform 320ms ease;
  pointer-events: none;
}

.app-demo[data-view="md"] .app-pane[data-pane="md"],
.app-demo[data-view="json"] .app-pane[data-pane="json"],
.app-demo[data-view="canvas"] .app-pane[data-pane="canvas"] {
  opacity: 1;
  transform: none;
  z-index: 1;
}

/* Staggered entrance. Base state is the finished state, so a pane that is not
   playing (reduced motion, or paused mid-cycle) still looks complete. */
@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

.app-pane.is-playing .rise {
  animation: rise 440ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  animation-delay: calc(var(--i, 0) * 60ms);
}

/* Hover pauses the loop, not the pane. Freezing the entrance animations would
   strand a pane at opacity 0 for anyone whose pointer lands mid-transition —
   the timer is the only thing that should stop. */
.app-demo.is-paused .demo-step .bar { animation-play-state: paused; }

/* --------------------------------------------- preview / source toggle --- */

.doc-source { display: none; }

.app-demo[data-mode="source"] .doc-preview { display: none; }
.app-demo[data-mode="source"] .doc-source { display: block; }

.src-code {
  height: 100%;
  margin: 0;
  overflow: hidden;
  color: var(--text);
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  line-height: 20px;
  white-space: pre-wrap;
  word-break: break-word;
}

.src-code code { background: none; padding: 0; border-radius: 0; }

.tok-key { color: var(--text); }
.tok-str, .tok-num, .tok-bool { color: var(--text-secondary); }
.tok-punc { color: var(--text-faint); }

/* ------------------------------------------------- properties (shared) --- */

.doc-page { height: 100%; padding: 32px 24px 20px 48px; overflow: hidden; }

.props { font-size: 12px; line-height: 16px; }

.props-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 28px;
  color: var(--text-faint);
  font-weight: 500;
  letter-spacing: 0.04em;
}

.props-head .ico {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 4px;
  color: var(--text-secondary);
}

.props-divided {
  margin-bottom: 20px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.prop-row {
  display: grid;
  grid-template-columns: minmax(96px, 1fr) minmax(160px, 2fr) 24px;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  padding: 2px 0;
}

.prop-key,
.prop-val {
  min-width: 0;
  padding: 4px 8px;
  border: 1px solid transparent;
  border-radius: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  line-height: 20px;
}

.prop-key { color: var(--text-secondary); }
.prop-val { color: var(--text); }
.prop-val.num { font-variant-numeric: tabular-nums; }

.tag-field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  min-height: 28px;
  padding: 4px;
  border: 1px solid transparent;
  border-radius: 4px;
}

.tag {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  background: var(--bg-tint);
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  white-space: nowrap;
}

/* ------------------------------------------------------ markdown pane ---- */

.md-body { color: var(--text); }

.md-body h1 {
  margin: 0 0 12px;
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 28px;
  line-height: 36px;
  font-weight: 700;
  letter-spacing: 0;
}

.md-body h2 {
  margin: 20px 0 12px;
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 24px;
  line-height: 32px;
  font-weight: 600;
}

.md-body p {
  margin: 0 0 12px;
  font-size: 16px;
  line-height: 24px;
}

.tasks { list-style: none; margin: 0; padding: 0 0 0 4px; }

.tasks li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 4px;
  font-size: 16px;
  line-height: 24px;
}

.cb {
  position: relative;
  flex: 0 0 16px;
  width: 16px;
  height: 16px;
  margin-top: 4px;
  border: 1px solid var(--text-secondary);
  border-radius: 4px;
  background: var(--bg);
}

.cb svg { position: absolute; inset: 0; opacity: 0; color: #fff; }

.cb.done { border-color: var(--accent); background: var(--accent); }
.cb.done svg { opacity: 1; }

/* A struck label is drawn, not just styled, so the beat reads as an action. */
.strike { position: relative; }

.strike::after {
  content: "";
  position: absolute;
  left: 0;
  top: 52%;
  height: 1px;
  width: 0;
  background: currentColor;
}

.strike.done { color: var(--text-secondary); }
.strike.done::after { width: 100%; }

/* The one narrative beat in this pane: a milestone gets checked off. */
@keyframes cb-fill {
  to { border-color: var(--accent); background: var(--accent); }
}
@keyframes cb-mark { to { opacity: 1; } }
@keyframes cb-pop {
  50% { transform: scale(1.18); }
}
@keyframes strike-draw { to { width: 100%; } }
@keyframes dim { to { color: var(--text-secondary); } }

.app-pane.is-playing .beat-cb {
  animation: cb-fill 200ms ease-out 2000ms forwards, cb-pop 280ms ease-out 2000ms;
}
.app-pane.is-playing .beat-cb svg { animation: cb-mark 160ms ease-out 2120ms forwards; }
.app-pane.is-playing .beat-strike { animation: dim 300ms ease-out 2200ms forwards; }
.app-pane.is-playing .beat-strike::after { animation: strike-draw 320ms ease-out 2200ms forwards; }

/* ---------------------------------------------------------- json pane ---- */

.grp-head {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 32px;
  padding-right: 8px;
  border-radius: 4px;
  font-size: 12px;
  line-height: 16px;
}

.grp-head .disc {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  color: var(--text-faint);
  transition: transform 240ms ease;
}

.grp-head .gname { color: var(--text); font-weight: 500; }
.grp-head .gcount { margin-left: auto; color: var(--text-faint); font-variant-numeric: tabular-nums; }

.grp-body {
  padding-left: 24px;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
}

.grp-body .prop-val { font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace; }

@keyframes grp-open {
  from { max-height: 0; opacity: 0; }
  to { max-height: 132px; opacity: 1; }
}
@keyframes disc-turn { to { transform: rotate(90deg); } }

.app-pane.is-playing .beat-grp { animation: grp-open 360ms ease-out 2600ms forwards; }
.app-pane.is-playing .beat-disc { animation: disc-turn 240ms ease-out 2600ms forwards; }

/* Boolean switch — the app's control, not a checkbox. */
.tg {
  position: relative;
  width: 40px;
  height: 24px;
  margin: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-hover);
}

.tg i {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 8px;
  background: var(--bg);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.24);
}

@keyframes tg-on {
  to { border-color: var(--accent); background: var(--accent); }
}
@keyframes tg-slide {
  to { left: 20px; }
}

.app-pane.is-playing .beat-tg { animation: tg-on 220ms ease-out 1700ms forwards; }
.app-pane.is-playing .beat-tg i { animation: tg-slide 260ms cubic-bezier(0.2, 0.8, 0.3, 1) 1700ms forwards; }

/* -------------------------------------------------------- canvas pane ---- */

.canvas-wrap {
  position: relative;
  height: 100%;
  background-color: var(--bg);
  background-image:
    linear-gradient(var(--border) 1px, transparent 1px),
    linear-gradient(90deg, var(--border) 1px, transparent 1px);
  background-size: 16px 16px;
}

.canvas-wrap svg.surface {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--text-secondary);
}

.cv-shape rect,
.cv-shape ellipse,
.cv-shape polygon {
  fill: var(--bg);
  stroke: currentColor;
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

.cv-label {
  fill: var(--text);
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 14px;
  text-anchor: middle;
}

.cv-arrow {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.cv-sel rect { fill: none; stroke: var(--accent); stroke-width: 1; vector-effect: non-scaling-stroke; }
.cv-port { fill: var(--bg); stroke: var(--accent); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.cv-port.hot { fill: var(--accent); }

.cv-shape, .cv-sel, .cv-port, .cv-halo { opacity: 1; }

@keyframes cv-in {
  from { opacity: 0; transform: scale(0.94); }
  to { opacity: 1; transform: none; }
}
@keyframes cv-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes cv-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes cv-halo { from { opacity: 0; transform: scale(0.4); } to { opacity: 1; transform: none; } }

.app-pane.is-playing .cv-shape {
  transform-box: fill-box;
  transform-origin: center;
  animation: cv-in 400ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  animation-delay: calc(var(--i, 0) * 110ms);
}

.app-pane.is-playing .cv-arrow {
  stroke-dasharray: 1;
  animation: cv-draw 520ms ease-out both;
  animation-delay: calc(440ms + var(--i, 0) * 200ms);
}

.app-pane.is-playing .cv-sel { animation: cv-fade 200ms ease-out 1600ms both; }

.app-pane.is-playing .cv-port {
  transform-box: fill-box;
  transform-origin: center;
  animation: cv-halo 260ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
  animation-delay: calc(1700ms + var(--i, 0) * 60ms);
}

.cv-toolbar,
.cv-zoom {
  position: absolute;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.cv-toolbar { top: 12px; left: 50%; transform: translateX(-50%); }
.cv-zoom { right: 12px; bottom: 12px; gap: 6px; color: var(--text-secondary); font-size: 12px; }

.cv-toolbar b,
.cv-zoom b {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 4px;
  color: var(--text-secondary);
}

.cv-toolbar b.on { color: var(--accent); background: var(--accent-bg); }
.cv-toolbar hr { width: 1px; height: 20px; margin: 0 2px; border: none; background: var(--border); }
.cv-zoom .pct { font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- steps --- */

.demo-steps {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin-top: 20px;
}

.demo-step {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 12px;
  border: 1px solid transparent;
  border-radius: 8px;
  overflow: hidden;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  line-height: 16px;
  color: var(--text-faint);
  background: none;
  cursor: pointer;
  transition: color 200ms ease, border-color 200ms ease;
}

.demo-step:hover { color: var(--text-secondary); border-color: var(--border); }
.demo-step:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
.demo-step[aria-pressed="true"] { color: var(--text); border-color: var(--border); }
.demo-step svg { flex-shrink: 0; }

.demo-step .bar {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 1px;
  width: 0;
  background: var(--accent);
}

@keyframes step-fill { from { width: 0; } to { width: 100%; } }
.demo-step.is-timing .bar { animation: step-fill var(--dwell, 5600ms) linear forwards; }

/* ---------- Bento features ----------
   Five cards, each with a looping demo of the feature it names. Every class
   here is prefixed (bento- for the section, bt- for tile internals) so the
   generic names these need — tile, stage, doc, node, edge — can't collide
   with the rest of the site. */

.bento-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 64px;
  margin-bottom: 48px;
}

.bento-head h2 {
  max-width: 380px;
  margin-bottom: 0;
  text-wrap: balance;
}

.bento-head p {
  font-size: 15px;
  line-height: 23px;
  color: var(--text-secondary);
  max-width: 440px;
  padding-top: 4px;
}

.bento-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: 470px 404px;
  gap: 24px;
}

/* No card padding: the gradient runs edge to edge and the copy carries its own
   inset, so the visual never sits in an inset panel-within-a-panel. */
.bento-tile {
  display: flex;
  flex-direction: column;
  background: var(--bg-tint);
  border: 1px solid var(--border);
  border-radius: 16px;
  overflow: hidden;
}

.bento-tile.is-wide {
  grid-column: span 2;
}

.bento-copy {
  padding: 18px 20px 20px;
  background: var(--bg);
}

.bento-copy h3 {
  font-size: 17px;
  line-height: 24px;
  margin-bottom: 6px;
}

.bento-copy p {
  font-size: 14px;
  line-height: 21px;
  color: var(--text-secondary);
}

/* Same forest photograph as the hero backdrop — replaces the earlier
   per-theme mesh gradient (--mesh-* tokens, now unused) with one
   consistent photo material behind all five cards. Deliberately
   theme-independent, same reasoning as the hero backdrop: it's a fixed
   photo, not a token-driven surface. overflow: hidden clips the blurred
   ::before layer below, which is intentionally larger than this box. */
.bento-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  overflow: hidden;
}

/* The photo itself lives on a blurred, oversized pseudo-element rather
   than directly on .bento-stage's background, for two reasons: blur
   softens the leaf/branch detail enough that the floating UI reads
   clearly on top instead of competing with a sharp photo, and it's what
   lets each card show a different crop (below) without the seam where a
   sharp image would visibly repeat. inset: -24px plus a slight scale
   pushes the blur's soft edge outside the visible box, so overflow:hidden
   crops it before the un-blurred/blurred boundary would show. Every card
   uses the same center crop by default; five are overridden below to
   different focal points of the same photo — otherwise five identical
   tiles of the same recognizable scene read as an obvious repeated tile
   rather than one consistent material. */
.bento-stage::before {
  content: "";
  position: absolute;
  inset: -24px;
  z-index: 0;
  background-image: url("assets/bento-bg.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  filter: blur(14px);
  transform: scale(1.08);
}

.bento-grid .bento-tile:nth-child(1) .bento-stage::before { background-position: 50% 15%; }
.bento-grid .bento-tile:nth-child(2) .bento-stage::before { background-position: 5% 10%; }
.bento-grid .bento-tile:nth-child(3) .bento-stage::before { background-position: 90% 85%; }
.bento-grid .bento-tile:nth-child(4) .bento-stage::before { background-position: 95% 5%; }
.bento-grid .bento-tile:nth-child(5) .bento-stage::before { background-position: 10% 90%; }

.bento-float {
  position: relative;
  z-index: 1;
  width: 100%;
  background: var(--bg);
  border-radius: 12px;
  box-shadow: var(--float-shadow);
}

/* ----- Tile 1: slash commands ----- */

/* Tall enough that the slash menu — a popover anchored under the caret — lands
   inside the document card instead of spilling onto the stage. */
.bt-doc {
  max-width: 480px;
  min-height: 258px;
  padding: 18px 20px 22px;
}

.bt-doc-h {
  font-size: 15px;
  line-height: 22px;
  font-weight: 700;
  margin-bottom: 8px;
}

.bt-doc-p {
  font-size: 13px;
  line-height: 20px;
  color: var(--text-secondary);
  margin-bottom: 10px;
}

/* Choosing "Checklist" converts the line the caret is on, so this line
   collapses the moment the checklist lands — the list then occupies the
   caret's position instead of appearing below an orphaned empty line. */
.bt-line {
  position: relative;
  height: 20px;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  line-height: 20px;
  color: var(--text);
  animation: bt-line-collapse 9s steps(1, end) infinite;
}

@keyframes bt-line-collapse {
  0%, 53% { height: 20px; }
  54%, 96% { height: 0; }
  97%, 100% { height: 20px; }
}

.bt-slash {
  opacity: 0;
  animation: bt-slash-in 9s linear infinite;
}

@keyframes bt-slash-in {
  0%, 8% { opacity: 0; }
  12%, 50% { opacity: 1; }
  54%, 100% { opacity: 0; }
}

/* Blink drives opacity, the collapse drives width — two properties, so the
   second animation doesn't cancel the first. */
.bt-caret {
  display: inline-block;
  width: 1.5px;
  height: 14px;
  vertical-align: -2px;
  background: var(--accent);
  animation: bt-caret-blink 1.05s step-end infinite, bt-caret-hide 9s steps(1, end) infinite;
}

@keyframes bt-caret-blink {
  50% { opacity: 0; }
}

@keyframes bt-caret-hide {
  0%, 53% { width: 1.5px; }
  54%, 96% { width: 0; }
  97%, 100% { width: 1.5px; }
}

.bt-menu {
  position: absolute;
  top: 26px;
  left: 0;
  width: 210px;
  padding: 5px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--float-shadow);
  opacity: 0;
  transform: translateY(4px) scale(0.98);
  transform-origin: top left;
  animation: bt-menu-in 9s ease-out infinite;
}

@keyframes bt-menu-in {
  0%, 13% { opacity: 0; transform: translateY(4px) scale(0.98); }
  18%, 46% { opacity: 1; transform: translateY(0) scale(1); }
  52%, 100% { opacity: 0; transform: translateY(-2px) scale(0.99); }
}

.bt-menu-hi {
  position: absolute;
  left: 5px;
  right: 5px;
  top: 5px;
  height: 26px;
  border-radius: 7px;
  background: var(--accent-bg);
  animation: bt-menu-hi-move 9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

@keyframes bt-menu-hi-move {
  0%, 24% { transform: translateY(0); }
  34%, 100% { transform: translateY(52px); }
}

.bt-menu-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  height: 26px;
  padding: 0 8px;
  font-size: 12px;
  color: var(--text-secondary);
}

.bt-menu-item svg {
  flex: none;
  width: 13px;
  height: 13px;
  color: var(--text-faint);
}

.bt-menu-item:nth-child(4) { color: var(--text); }
.bt-menu-item:nth-child(4) svg { color: var(--accent); }

.bt-task {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  font-size: 13px;
  line-height: 20px;
  color: var(--text-secondary);
  opacity: 0;
  animation: bt-task-in 9s ease-out infinite;
}

/* No leading gap: the first item sits exactly where the collapsed caret was. */
.bt-task-1 { margin-top: 0; }
.bt-task-2 { animation-delay: 0.1s; }
.bt-task-3 { animation-delay: 0.2s; }

@keyframes bt-task-in {
  0%, 54% { opacity: 0; transform: translateY(4px); }
  60%, 94% { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(0); }
}

.bt-box {
  position: relative;
  flex: none;
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--text-faint);
  border-radius: 3.5px;
  animation: bt-box-on 9s ease-out infinite;
}

.bt-box svg {
  position: absolute;
  inset: 1px;
  width: 8px;
  height: 8px;
  opacity: 0;
  stroke-dasharray: 16;
  stroke-dashoffset: 16;
  animation: bt-box-draw 9s ease-out infinite;
}

@keyframes bt-box-on {
  0%, 68% { border-color: var(--text-faint); background: transparent; }
  74%, 94% { border-color: var(--accent); background: var(--accent-bg); }
  100% { border-color: var(--text-faint); background: transparent; }
}

@keyframes bt-box-draw {
  0%, 70% { opacity: 0; stroke-dashoffset: 16; }
  78%, 94% { opacity: 1; stroke-dashoffset: 0; }
  100% { opacity: 0; stroke-dashoffset: 16; }
}

.bt-box-idle {
  flex: none;
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--text-faint);
  border-radius: 3.5px;
}

.bt-task-text { position: relative; }

.bt-task-text::after {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  animation: bt-strike 9s ease-out infinite;
}

@keyframes bt-strike {
  0%, 72% { transform: scaleX(0); }
  82%, 94% { transform: scaleX(1); }
  100% { transform: scaleX(0); }
}

/* ----- Tile 2: canvas -----
   Shapes and arrows share one SVG coordinate space, so the connectors actually
   terminate on the shapes instead of being positioned independently. */

.bt-canvas {
  position: relative;
  width: 100%;
  height: 100%;
}

.bt-board {
  display: block;
  width: 100%;
  height: 100%;
}

.bt-node {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  animation: bt-node-in 10s cubic-bezier(0.34, 1.3, 0.5, 1) infinite;
}

.bt-node rect {
  fill: var(--bg);
  stroke: var(--border);
  stroke-width: 1.5;
}

.bt-node text {
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 12px;
  font-weight: 500;
  fill: var(--text-secondary);
  text-anchor: middle;
  dominant-baseline: middle;
}

.bt-node.is-accent rect { stroke: var(--accent); }
.bt-node.is-accent text { fill: var(--accent); }
.bt-node.is-note rect { fill: var(--accent-tint); stroke: none; }
.bt-node.is-note text { fill: var(--accent-deep); font-size: 11px; }

:root[data-theme="dark"] .bt-node.is-note text { fill: var(--accent); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .bt-node.is-note text { fill: var(--accent); }
}

.bt-node:nth-of-type(2) { animation-delay: 0.15s; }
.bt-node:nth-of-type(3) { animation-delay: 0.3s; }
.bt-node:nth-of-type(4) { animation-delay: 0.45s; }

@keyframes bt-node-in {
  0%, 3% { opacity: 0; transform: translateY(7px) scale(0.92); }
  13%, 100% { opacity: 1; transform: translateY(0) scale(1); }
}

.bt-edge {
  fill: none;
  stroke: var(--text-faint);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-dasharray: 200;
  stroke-dashoffset: 200;
  animation: bt-edge-draw 10s ease-in-out infinite;
}

.bt-edge-2 { animation-delay: 0.5s; }

/* Holds drawn through 100% and cuts back on the loop boundary — a fade-out
   leaves the board visibly blank, which reads as the diagram dying. */
@keyframes bt-edge-draw {
  0%, 20% { stroke-dashoffset: 200; opacity: 0; }
  24% { opacity: 1; }
  44%, 100% { stroke-dashoffset: 0; opacity: 1; }
}

.bt-head {
  fill: var(--text-faint);
  opacity: 0;
  animation: bt-head-in 10s ease-out infinite;
}

.bt-head-2 { animation-delay: 0.5s; }

@keyframes bt-head-in {
  0%, 42% { opacity: 0; }
  48%, 100% { opacity: 1; }
}

/* Toolbar — the active tool tracks whatever is being drawn. */
.bt-tools {
  position: absolute;
  bottom: 0;
  left: 50%;
  z-index: 2;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--float-shadow);
}

.bt-tool {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  color: var(--text-faint);
}

.bt-tool svg { width: 14px; height: 14px; }

.bt-tool-hi {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  background: var(--accent-bg);
  animation: bt-tool-move 10s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

@keyframes bt-tool-move {
  0%, 2% { transform: translateX(0); }
  8%, 18% { transform: translateX(56px); }
  26%, 46% { transform: translateX(112px); }
  54%, 100% { transform: translateX(0); }
}

/* ----- Tile 3: command palette ----- */

.bt-palette {
  max-width: 320px;
  overflow: hidden;
}

.bt-pal-bar {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 12px;
  border-bottom: 1px solid var(--border);
}

.bt-pal-bar > svg {
  flex: none;
  width: 14px;
  height: 14px;
  color: var(--text-faint);
}

/* line-height must match the box height, or the inherited body leading drops
   the query text below the search icon's centre line. */
.bt-pal-q {
  position: relative;
  flex: 1;
  height: 18px;
  font-size: 13px;
  line-height: 18px;
  color: var(--text);
}

.bt-pal-ph {
  position: absolute;
  inset: 0;
  line-height: 18px;
  color: var(--text-faint);
  animation: bt-ph-out 14s linear infinite;
}

/* The placeholder returns only in the gap between the two queries, so it never
   sits underneath typed text. */
@keyframes bt-ph-out {
  0%, 4% { opacity: 1; }
  8%, 46% { opacity: 0; }
  50% { opacity: 1; }
  54%, 96% { opacity: 0; }
  99%, 100% { opacity: 1; }
}

.bt-pal-typed {
  position: absolute;
  inset: 0;
  width: 0;
  line-height: 18px;
  white-space: nowrap;
  overflow: hidden;
}

.bt-pal-q1 { animation: bt-type-1 14s steps(7, end) infinite; }

@keyframes bt-type-1 {
  0%, 6% { width: 0; }
  18%, 44% { width: 4.6em; }
  48%, 100% { width: 0; }
}

.bt-pal-q2 {
  color: var(--accent);
  font-weight: 600;
  animation: bt-type-2 14s steps(1, end) infinite;
}

@keyframes bt-type-2 {
  0%, 52% { width: 0; }
  56%, 94% { width: 1.2em; }
  97%, 100% { width: 0; }
}

.bt-pal-kbd {
  flex: none;
  font-size: 10px;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--bg-tint);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 5px;
}

.bt-pal-groups {
  position: relative;
  height: 106px;
}

.bt-pal-group {
  position: absolute;
  inset: 0;
  padding: 5px;
  opacity: 0;
}

.bt-pal-files { animation: bt-grp-a 14s ease-out infinite; }
.bt-pal-actions { animation: bt-grp-b 14s ease-out infinite; }

@keyframes bt-grp-a {
  0%, 8% { opacity: 0; }
  14%, 44% { opacity: 1; }
  48%, 100% { opacity: 0; }
}

@keyframes bt-grp-b {
  0%, 54% { opacity: 0; }
  60%, 94% { opacity: 1; }
  97%, 100% { opacity: 0; }
}

.bt-pal-hi {
  position: absolute;
  left: 5px;
  right: 5px;
  top: 5px;
  height: 32px;
  border-radius: 7px;
  background: var(--accent-bg);
}

.bt-pal-files .bt-pal-hi {
  animation: bt-pal-hi-move 14s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

@keyframes bt-pal-hi-move {
  0%, 24% { transform: translateY(0); }
  32%, 100% { transform: translateY(64px); }
}

.bt-pal-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  height: 32px;
  padding: 0 8px;
  font-size: 12.5px;
  color: var(--text);
}

.bt-pal-row svg {
  flex: none;
  width: 13px;
  height: 13px;
  color: var(--text-faint);
}

.bt-pal-actions .bt-pal-row svg { color: var(--accent); }

.bt-pal-path {
  margin-left: auto;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 10.5px;
  color: var(--text-faint);
}

/* ----- Tile 4: config files ----- */

.bt-cfg {
  max-width: 300px;
  overflow: hidden;
}

.bt-cfg-head {
  position: relative;
  height: 38px;
  border-bottom: 1px solid var(--border);
}

.bt-cfg-name {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  font-size: 12.5px;
  font-weight: 600;
  opacity: 0;
  animation: bt-cfg-swap 10.5s ease-in-out infinite;
}

.bt-cfg-name svg {
  flex: none;
  width: 13px;
  height: 13px;
  color: var(--accent);
}

.bt-cfg-name:nth-of-type(2),
.bt-cfg-body:nth-of-type(2) { animation-delay: 3.5s; }

.bt-cfg-name:nth-of-type(3),
.bt-cfg-body:nth-of-type(3) { animation-delay: 7s; }

.bt-cfg-bodies {
  position: relative;
  height: 108px;
}

.bt-cfg-body {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 12px;
  opacity: 0;
  animation: bt-cfg-swap 10.5s ease-in-out infinite;
}

@keyframes bt-cfg-swap {
  0% { opacity: 0; transform: translateY(6px); }
  5%, 30% { opacity: 1; transform: translateY(0); }
  35%, 100% { opacity: 0; transform: translateY(-6px); }
}

.bt-cfg-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  line-height: 16px;
  color: var(--text);
}

.bt-k { color: var(--text-secondary); }

.bt-v {
  margin-left: auto;
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  color: var(--text-faint);
}

.bt-mask { letter-spacing: 0.12em; }

.bt-tick {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--accent);
  border-radius: 3.5px;
  background: var(--accent-bg);
  color: var(--accent);
}

.bt-tick svg { width: 9px; height: 9px; }

.bt-dot {
  flex: none;
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--text-faint);
  border-radius: 3.5px;
}

.bt-keyico {
  flex: none;
  width: 13px;
  height: 13px;
  color: var(--text-faint);
}

/* ----- Tile 5: HTML preview ----- */

.bt-browser {
  max-width: 300px;
  overflow: hidden;
}

.bt-br-chrome {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
}

.bt-br-chrome i {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--border);
}

.bt-br-url {
  flex: 1;
  margin-left: 4px;
  height: 16px;
  padding: 0 6px;
  border-radius: 4px;
  background: var(--bg-tint);
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  font-size: 9.5px;
  line-height: 16px;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
}

/* A rendered page wireframe — nav then hero — laid in as the page paints. */
.bt-br-page {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 10px;
}

.bt-br-el {
  opacity: 0;
  animation: bt-paint-in 7s ease-out infinite;
}

.bt-br-el:nth-child(2) { animation-delay: 0.14s; }

@keyframes bt-paint-in {
  0% { opacity: 0; transform: translateY(5px); }
  12%, 100% { opacity: 1; transform: translateY(0); }
}

.bt-br-nav {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--border);
}

.bt-br-logo {
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: 4px;
  background: var(--accent);
}

.bt-br-link {
  width: 20px;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}

.bt-br-link:first-of-type { margin-left: 4px; }

.bt-br-btn {
  margin-left: auto;
  width: 34px;
  height: 13px;
  border: 1px solid var(--accent);
  border-radius: 4px;
  background: var(--accent-bg);
}

.bt-br-hero {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 2px 0 4px;
}

.bt-br-h {
  width: 74%;
  height: 10px;
  border-radius: 4px;
  background: var(--text-faint);
  opacity: 0.55;
}

.bt-br-h.is-short { width: 52%; }

.bt-br-sub {
  width: 88%;
  height: 5px;
  margin-top: 2px;
  border-radius: 3px;
  background: var(--border);
}

.bt-br-cta {
  width: 52px;
  height: 16px;
  margin-top: 4px;
  border-radius: 5px;
  background: var(--accent);
}

/* ---------- FAQ ---------- */

/* Up from the base h2's 32px, matching .bento-head's own bump below so the
   two section headings sit the same distance from their content. */
.faq h2 {
  margin-bottom: 48px;
}

.faq details {
  border-bottom: 1px solid var(--border);
}

.faq details:last-of-type {
  border-bottom: none;
}

.faq summary {
  font-size: 24px;
  line-height: 32px;
  font-weight: 500;
  padding: 24px 0;
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

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

.faq summary::after {
  content: "+";
  color: var(--text-faint);
  font-weight: 400;
}

.faq details[open] summary::after {
  content: "–";
}

.faq summary:hover {
  color: var(--accent);
}

/* Matches .faq summary's own 24px, so an open row keeps the same 24px inset
   above its question and below its answer rather than tightening at the
   bottom edge. */
.faq details p {
  color: var(--text-secondary);
  padding-bottom: 24px;
}

/* The container widened to fit the bento grid, but running prose still needs a
   readable measure — full-width body copy is ~180 characters a line. Summary
   rows and headings are exempt: those are meant to span the container. */
.faq details p,
.changelog-header p,
.release li,
.help-header p,
.help-topic p,
.help-topic li {
  max-width: 68ch;
}

/* ---------- Changelog & Help (shared page-header pattern) ---------- */

.changelog-header,
.help-header {
  padding: 64px 0 0;
  border-bottom: none;
}

.changelog-header h1,
.help-header h1 {
  font-family: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 32px;
  line-height: 40px;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-bottom: 8px;
}

.changelog-header p,
.help-header p {
  color: var(--text-secondary);
}

/* ---------- Help ---------- */

.help-topic {
  padding: 48px 0;
}

/* Doc-page topic headings read as sub-sections, not homepage-scale section
   heads, so they stay at the smaller 24/32 scale rather than the 32/40 base. */
.help-topic h2 {
  font-size: 24px;
  line-height: 32px;
  margin-bottom: 12px;
}

.help-topic > p {
  color: var(--text-secondary);
  margin-bottom: 16px;
}

.help-topic ol {
  padding-left: 20px;
  margin-bottom: 16px;
}

.help-topic li {
  color: var(--text-secondary);
  padding-left: 4px;
  margin-bottom: 8px;
}

.help-topic li::marker {
  color: var(--text-faint);
}

/* Inline prose links (the support email) fell back to the browser default
   blue in dark mode — no generic body-link style exists elsewhere on the
   site, since every other link is a nav/footer item styled separately. */
.help-topic a {
  color: var(--accent);
}

.help-topic a:hover {
  color: var(--text);
}

/* Two-column layout: a narrow rail carries the version/badge/date, the
   wide column carries the title + bullets — makes use of the page's
   1152px width instead of one plain centered stack, and reads closer to
   a real product changelog (Stripe/Linear) as more releases land. */
.release {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 32px;
  padding: 48px 0;
}

/* Same reasoning as .footer-cta's own padding-bottom on the homepage: the
   last release before the footer's tinted, bordered block needs more
   separation than one release card gets from the next. */
.release:last-of-type {
  padding-bottom: 112px;
}

.release-rail {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}

.release-rail h2 {
  font-size: 16px;
  line-height: 24px;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 0;
}

.badge {
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: var(--accent);
  background: var(--accent-bg);
  padding: 0 8px;
  border-radius: 8px;
}

.release-date {
  font-size: 12px;
  line-height: 16px;
  color: var(--text-faint);
}

.release h3 {
  font-family: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 24px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 16px;
}

.release ul {
  list-style: none;
}

.release li {
  position: relative;
  color: var(--text-secondary);
  padding-left: 20px;
  margin-bottom: 8px;
}

.release li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 10px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-faint);
}

/* ---------- Footer CTA ----------
   Plain background, not a dark panel — the section break itself (the
   standard border-bottom every section gets) is enough separation, and it
   keeps the page's one accent moment on the button, not a whole block. */

/* padding-bottom widened past the standard 96px section rhythm — the last
   thing on the page before the footer's own tinted, bordered block needs
   more separation than a plain section-to-section boundary gets, so the
   footer reads as a deliberate close rather than just another section. */
.footer-cta {
  text-align: center;
  padding-bottom: 160px;
}

.footer-cta img {
  border-radius: 8px;
  margin-bottom: 24px;
}

.footer-cta h2 {
  margin-bottom: 12px;
}

.footer-cta .cta-sub {
  font-size: 16px;
  line-height: 24px;
  color: var(--text-secondary);
  max-width: 560px;
  margin: 0 auto 24px;
}

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

/* padding-top matches the standard 96px section rhythm (see "Widened
   section rhythm" — every section carries 96px top/bottom padding, so two
   adjacent sections' content sits 192px apart: 96 from the one above plus
   96 from the one below). .footer is a <footer>, not a section, so it
   never inherited that 96 — it shipped with a smaller 64px, which meant
   the CTA button sat closer to the footer's own content (96 + 64 = 160px)
   than the FAQ sits to the CTA above it (96 + 96 = 192px), a rhythm break
   right at the bottom of the page. 96px makes the two gaps equal. */
.footer {
  border-top: 1px solid var(--border);
  background: var(--bg-tint);
  padding: 96px 24px 0;
}

.footer-inner {
  max-width: 1152px;
  margin: 0 auto;
}

.footer-row {
  display: flex;
  justify-content: space-between;
  gap: 48px;
  padding-bottom: 48px;
}

.footer-brand {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 280px;
}

.footer-brand .brand {
  font-size: 16px;
}

.footer-brand p {
  font-size: 14px;
  line-height: 20px;
  color: var(--text-secondary);
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 64px;
}

.footer-links h3 {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  margin-bottom: 12px;
}

.footer-links ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.footer-links a {
  font-size: 14px;
  line-height: 20px;
  color: var(--text-secondary);
  text-decoration: none;
}

.footer-links a:hover {
  color: var(--text);
}

.footer-bottom {
  border-top: 1px solid var(--border);
  padding: 24px 0;
  font-size: 12px;
  line-height: 20px;
  color: var(--text-faint);
}

/* ---------- Tablet ---------- */

@media (max-width: 980px) {
  .bento-head {
    flex-direction: column;
    gap: 16px;
  }

  .bento-head h2 {
    max-width: none;
  }

  /* Two columns with the markdown card across the top; explicit row heights
     stop applying so each card can size to its own visual. */
  .bento-grid {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: none;
  }

  .bento-tile {
    min-height: 400px;
  }

  /* Same 1.2545 scale-up as the base rule above, applied to this
     breakpoint's own original 180/460 values. */
  .app-demo {
    --sidebar-w: 226px;
    --frame-h: 577px;
  }

  /* One step down from the 64px desktop headline — at tablet widths the
     720px measure is most of the column, and 64px starts wrapping into
     three lines. Direct-child selector, same reasoning as the base rule
     above — a plain descendant selector would also resize the app demo's
     nested "Pokedex" <h1>. */
  .hero > h1 {
    font-size: 48px;
    line-height: 52px;
  }

  /* Previously left at the desktop 18/27 size, which meant the H1 stepped
     down (64px → 48px, a 0.75× cut) while the lede stayed completely flat
     — the two stopped scaling together and the lede started reading as
     oversized next to the shrunken headline. Steps down alongside it, just
     at a gentler rate than the headline (0.75× vs ~0.89×), since body copy
     needs to stay comfortably readable even as a display headline shrinks
     hard; line-height keeps the same 1.5× ratio as every other step. */
  .lede {
    font-size: 16px;
    line-height: 24px;
  }

  /* The demo frame is shorter here, so the backdrop's edge has to come up
     with it to stay ~65% of the way down the window. */
  .home {
    --hero-backdrop-h: 910px;
  }

  /* One step down from the 24/32 desktop size, same 1.333 ratio. */
  .faq summary {
    font-size: 20px;
    line-height: 28px;
  }
}

/* ---------- Mobile ---------- */

@media (max-width: 640px) {
  .nav-links {
    display: none;
  }

  section {
    padding: 48px 0;
  }

  .hero {
    padding-top: 64px;
  }

  .hero > h1 {
    font-size: 36px;
    line-height: 42px;
  }

  /* Let the narrow column choose its own wrap on phones. */
  .hero-break {
    display: none;
  }

  /* Continues the tablet step's cut down from the desktop 18/27, rather
     than repeating the same 16/24 — otherwise the lede goes flat again
     right where the H1 takes its own second cut (48px → 36px). 21/14 = 1.5,
     the same line-height ratio held at every step. */
  .lede {
    font-size: 14px;
    line-height: 21px;
  }

  h2 {
    font-size: 32px;
    line-height: 40px;
  }

  /* One further step down from the tablet 20/28. */
  .faq summary {
    font-size: 18px;
    line-height: 26px;
  }

  /* Same 1.2545 scale-up as the base rule, applied to this breakpoint's
     own original 400px value. */
  .app-demo {
    --frame-h: 502px;
  }

  /* Shorter hero (smaller headline, tighter top padding) and a shorter
     demo frame, so the backdrop's edge tracks both up the page. Tuned
     against the 375px viewport rather than derived — the headline's own
     wrap count moves with the text. */
  .home {
    --hero-backdrop-h: 890px;
  }

  .app-sidebar {
    display: none;
  }

  .doc-page {
    padding: 16px;
  }

  .md-body h1 {
    font-size: 24px;
    line-height: 32px;
  }

  .md-body h2 {
    font-size: 20px;
    line-height: 28px;
  }

  .bento-grid {
    grid-template-columns: 1fr;
    gap: 16px;
  }

  .bento-tile,
  .bento-tile.is-wide {
    grid-column: span 1;
    min-height: 360px;
  }

  .bento-stage {
    padding: 20px;
  }

  .footer-row {
    flex-direction: column;
    gap: 32px;
  }

  .footer-links {
    gap: 40px;
  }

  .release {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .release-rail {
    flex-direction: row;
    align-items: center;
  }
}

/* ---------- Scroll reveal ----------
   Sections and cards fade up into place the first time they cross into the
   viewport (IntersectionObserver in index.html). Base state is the
   finished, visible state — .will-reveal is what the script opts an
   element into, so a no-JS visitor never sees hidden content. */

.reveal.will-reveal {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 500ms cubic-bezier(0.2, 0.7, 0.3, 1),
    transform 500ms cubic-bezier(0.2, 0.7, 0.3, 1);
}

.reveal.will-reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* Gaps wider than the transition itself so each item visibly finishes
   settling before the next one starts, rather than all blurring together. */
.bento-grid .bento-tile:nth-child(2) { transition-delay: 140ms; }
.bento-grid .bento-tile:nth-child(3) { transition-delay: 280ms; }
.bento-grid .bento-tile:nth-child(4) { transition-delay: 420ms; }
.bento-grid .bento-tile:nth-child(5) { transition-delay: 560ms; }

.faq details:nth-of-type(2) { transition-delay: 110ms; }
.faq details:nth-of-type(3) { transition-delay: 220ms; }
.faq details:nth-of-type(4) { transition-delay: 330ms; }
.faq details:nth-of-type(5) { transition-delay: 440ms; }
.faq details:nth-of-type(6) { transition-delay: 550ms; }
.faq details:nth-of-type(7) { transition-delay: 660ms; }

/* ---------- Reduced motion ----------
   Each bento loop rests on the single frame that reads clearest on its own. */

@media (prefers-reduced-motion: reduce) {
  /* The hero demo drives its own cross-fade with JS + CSS transitions rather
     than the bento cards' pure-keyframe loops, so it needs a blanket freeze
     instead of per-property overrides — the script itself also checks this
     query and stops auto-advancing (see index.html). */
  .app-demo * {
    animation: none !important;
    transition-duration: 1ms !important;
  }

  .bt-line, .bt-slash, .bt-caret, .bt-menu, .bt-menu-hi, .bt-task, .bt-box,
  .bt-box svg, .bt-task-text::after, .bt-node, .bt-edge, .bt-head, .bt-tool-hi,
  .bt-pal-ph, .bt-pal-typed, .bt-pal-group, .bt-pal-hi, .bt-cfg-name,
  .bt-cfg-body, .bt-br-el {
    animation: none !important;
  }

  /* Card 1 rests on the slash menu open at the caret, so the finished
     checklist is hidden — showing both at once contradicts itself. */
  .bt-menu { opacity: 1; transform: none; }
  .bt-menu-hi { transform: translateY(52px); }
  .bt-slash { opacity: 1; }
  .bt-task { display: none; }

  .bt-node, .bt-head, .bt-br-el { opacity: 1; transform: none; }
  .bt-edge { stroke-dashoffset: 0; opacity: 1; }
  .bt-tool-hi { transform: translateX(0); }

  .bt-pal-ph { opacity: 0; }
  .bt-pal-q1 { width: 4.6em; }
  .bt-pal-q2 { width: 0; }
  .bt-pal-files { opacity: 1; }
  .bt-pal-files .bt-pal-hi { transform: translateY(64px); }

  .bt-cfg-name:nth-of-type(1),
  .bt-cfg-body:nth-of-type(1) { opacity: 1; transform: none; }

  .eyebrow-dot::after {
    animation: none !important;
    opacity: 0 !important;
  }

  .reveal.will-reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  .nav {
    transition: none !important;
  }
}
