:root {
  --background: 210 33% 98%;
  --foreground: 217 48% 20%;
  --card: 0 0% 100%;
  --card-foreground: 217 48% 20%;
  --muted: 210 20% 95%;
  --muted-foreground: 216 14% 30%;
  --primary: 217 48% 20%;
  --primary-foreground: 0 0% 100%;
  --accent: 207 55% 40%;
  --accent-foreground: 0 0% 100%;
  --secondary: 207 55% 40%;
  --secondary-foreground: 0 0% 100%;
  --border: 214 32% 91%;
  --input: 214 32% 91%;
  --destructive: 0 72% 51%;
  --success: 160 84% 26%;
  --warning: 37 92% 31%;
  --navy: 217 48% 20%;
  --blue: 207 55% 40%;
  --radius: 0.625rem;
}

.dark {
  /* Lifted from 8% → 14% so dark-mode product screenshots (which use
     their own ~6–9% dark surfaces) stand off the page rather than
     blending into it. Cards/muted/border re-tiered to keep visible
     elevation between surface layers. */
  --background: 220 18% 14%;
  --foreground: 210 33% 95%;
  --card: 220 16% 18%;
  --card-foreground: 210 33% 95%;
  --muted: 220 14% 22%;
  --muted-foreground: 216 14% 74%;
  --primary: 210 33% 95%;
  --primary-foreground: 220 20% 8%;
  /* Raised from 48% → 62% so accent-as-text clears 4.5:1 on dark card (#272b35: 5.25)
     and dark muted (#303540: 4.55). Accent-bg buttons use a darker hardcoded override below. */
  --accent: 207 55% 62%;
  --accent-foreground: 0 0% 100%;
  --secondary: 207 55% 62%;
  --secondary-foreground: 222 47% 11%;
  --border: 220 14% 28%;
  --input: 220 14% 28%;
  /* Lifted for dark-bg contrast: success/warning as text on dark cards must clear 4.5:1. */
  --success: 160 84% 50%;
  --warning: 37 92% 62%;
}

/* ── Dark-mode: accent-colored button backgrounds ──────────────────────────
   --accent is lifted to 62% for text-on-dark-surface legibility, but that
   makes it too light for white text (contrast < 4.5:1). Override the bg for
   any element that uses hsl(var(--accent)) as its background so buttons keep
   a dark-enough fill to pass 4.5:1 with white text (L=40% → 5.62:1). */
.dark .bg-\[hsl\(var\(--accent\)\)\] {
  background-color: hsl(207 55% 40%);
}

/* ── Accent text on an accent tint (the chip pattern) ───────────────────────
   bg-[hsl(var(--accent)/0.1x)] + text-[hsl(var(--accent))] is the fleet's chip:
   badges, exam codes, step circles, icon tiles. The tint pulls the chip's fill
   toward its own text, and on the 2026-09-23 axe sweep small text fell under
   4.5:1 (4.08-4.49:1 in dark mode, 4.46:1 in light; a /0.15 chip over --muted
   computes to 4.10:1). On a tint the text steps one shade away from its fill,
   L=34% in light and L=72% in dark, which clears 4.5:1 on card, background,
   and muted at every tint listed. scripts/test_theme_contrast.py measures each
   one and fails when a template uses a tint missing from this list. */
.bg-\[hsl\(var\(--accent\)\/0\.1\)\].text-\[hsl\(var\(--accent\)\)\],
.bg-\[hsl\(var\(--accent\)\/0\.10\)\].text-\[hsl\(var\(--accent\)\)\],
.bg-\[hsl\(var\(--accent\)\/0\.12\)\].text-\[hsl\(var\(--accent\)\)\],
.bg-\[hsl\(var\(--accent\)\/0\.15\)\].text-\[hsl\(var\(--accent\)\)\] {
  color: hsl(207 55% 34%);
}
.dark .bg-\[hsl\(var\(--accent\)\/0\.1\)\].text-\[hsl\(var\(--accent\)\)\],
.dark .bg-\[hsl\(var\(--accent\)\/0\.10\)\].text-\[hsl\(var\(--accent\)\)\],
.dark .bg-\[hsl\(var\(--accent\)\/0\.12\)\].text-\[hsl\(var\(--accent\)\)\],
.dark .bg-\[hsl\(var\(--accent\)\/0\.15\)\].text-\[hsl\(var\(--accent\)\)\] {
  color: hsl(207 55% 72%);
}

/* ── Status text on a status tint (success, warning, destructive chips) ─────
   The accent chip's problem in the status colors: lab difficulty chips on the
   course pages measured 4.27-4.32:1 in light mode (axe, 2026-09-24), the
   tinted "Live" and "Soon" chips fall to about 4.1:1 on --background and
   --muted, and --destructive, which the dark block never lifts, reads 2.45:1
   on a dark tint. On a tint the text takes a deeper shade in light mode and a
   lifted one in dark, which clears 4.5:1 on card, background, and muted at
   every tint listed. scripts/test_theme_contrast.py measures each one and
   fails when a template uses a status tint missing from this list. */
.bg-\[hsl\(var\(--success\)\/0\.1\)\].text-\[hsl\(var\(--success\)\)\],
.bg-\[hsl\(var\(--success\)\/0\.12\)\].text-\[hsl\(var\(--success\)\)\] {
  color: hsl(160 84% 24%);
}
.bg-\[hsl\(var\(--warning\)\/0\.1\)\].text-\[hsl\(var\(--warning\)\)\],
.bg-\[hsl\(var\(--warning\)\/0\.12\)\].text-\[hsl\(var\(--warning\)\)\],
.bg-\[hsl\(var\(--warning\)\/0\.15\)\].text-\[hsl\(var\(--warning\)\)\] {
  color: hsl(37 92% 28%);
}
.bg-\[hsl\(var\(--destructive\)\/0\.1\)\].text-\[hsl\(var\(--destructive\)\)\] {
  color: hsl(0 72% 44%);
}
.dark .bg-\[hsl\(var\(--success\)\/0\.1\)\].text-\[hsl\(var\(--success\)\)\],
.dark .bg-\[hsl\(var\(--success\)\/0\.12\)\].text-\[hsl\(var\(--success\)\)\] {
  color: hsl(var(--success));
}
.dark .bg-\[hsl\(var\(--warning\)\/0\.1\)\].text-\[hsl\(var\(--warning\)\)\],
.dark .bg-\[hsl\(var\(--warning\)\/0\.12\)\].text-\[hsl\(var\(--warning\)\)\],
.dark .bg-\[hsl\(var\(--warning\)\/0\.15\)\].text-\[hsl\(var\(--warning\)\)\] {
  color: hsl(var(--warning));
}
.dark .bg-\[hsl\(var\(--destructive\)\/0\.1\)\].text-\[hsl\(var\(--destructive\)\)\] {
  color: hsl(0 72% 72%);
}

/* ── Dark-mode: solid-success-green backgrounds ─────────────────────────────
   Light-mode --success (#0b7a55 at L=26%) is fine; dark --success is lifted
   to L=50% for text legibility, but as a solid badge/button background it
   fails 4.5:1 with white text (#14eba3 on white = 1.56). Override to a darker
   teal (L=26% = #0b7a55) so white text hits 5.35:1. */
.dark .bg-\[hsl\(var\(--success\)\)\] {
  background-color: hsl(160 84% 26%);
}

/* ── The offer badges (partials/_trial.jinja offer_badge) ────────────────────
   The owner, 2026-09-24, 3:15 PM CDT: "need a way to indicate that those are
   free and do not require credit cards and the others are 7-day trial with
   credit card required." Two badges, one shape: a free product's in emerald,
   the trial's in the accent, each with its own icon. The text takes the chip
   shades above (a deeper one in light mode, a lifted one in dark), so it clears
   4.5:1 on its tint over card, background, and muted in both themes. On a band
   that is dark in both themes (`as-offer--on-dark`: a photo hero, the accent
   gradient) the fill is solid and the text white. scripts/test_theme_contrast.py
   measures every pair. */
.as-offer {
  display: inline-flex;
  align-items: flex-start;
  gap: 0.375rem;
  max-width: 100%;
  padding: 0.25rem 0.625rem;
  border-width: 1px;
  border-style: solid;
  border-radius: 0.5rem;
  font-size: 0.75rem;
  line-height: 1.125rem;
  font-weight: 500;
  text-align: left;
  vertical-align: middle;
}
.as-offer__icon { width: 0.875rem; height: 0.875rem; flex-shrink: 0; margin-top: 0.125rem; }
.as-offer__label { font-weight: 700; }
.as-offer--free {
  color: hsl(160 84% 21%);
  background-color: hsl(160 84% 26% / 0.1);
  border-color: hsl(160 84% 26% / 0.35);
}
.as-offer--trial {
  color: hsl(207 55% 34%);
  background-color: hsl(207 55% 40% / 0.1);
  border-color: hsl(207 55% 40% / 0.35);
}
.dark .as-offer--free {
  color: hsl(160 84% 50%);
  background-color: hsl(160 84% 50% / 0.12);
  border-color: hsl(160 84% 50% / 0.35);
}
.dark .as-offer--trial {
  color: hsl(207 55% 72%);
  background-color: hsl(207 55% 62% / 0.12);
  border-color: hsl(207 55% 62% / 0.4);
}
.as-offer.as-offer--on-dark.as-offer--free,
.dark .as-offer.as-offer--on-dark.as-offer--free {
  color: #ffffff;
  background-color: hsl(160 84% 22%);
  border-color: hsl(0 0% 100% / 0.3);
}
.as-offer.as-offer--on-dark.as-offer--trial,
.dark .as-offer.as-offer--on-dark.as-offer--trial {
  color: #ffffff;
  background-color: hsl(207 55% 32%);
  border-color: hsl(0 0% 100% / 0.3);
}

* { box-sizing: border-box; }

/* <picture> as a layout-transparent wrapper.
   Both hero mechanisms (the rotating hero_photo macro and the fixed-pair
   partials/_theme_hero_image.jinja) wrap their <img> in a <picture> so the
   browser can pick ONE file from a prefers-color-scheme <source>. That
   wrapper must not participate in layout: an inline <picture> shrinks to
   content, which would break the `w-full h-full` inline use the fixed-pair
   partial documents. `display: contents` removes the box while leaving the
   resource-selection behavior — which the HTML image algorithm runs
   independently of CSS — completely intact. */
picture.as-hero-pic { display: contents; }

/* Theme-aware hero image swap.
   Stack two <img> elements absolutely on top of each other and toggle
   visibility based on the `.dark` class on <html>. Both stay in flow so the
   active one paints immediately on theme change with no reload flash.
   Use via partials/_theme_hero_image.jinja. */
.theme-hero-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 200ms ease;
}
.theme-hero-light { opacity: 1; }
.theme-hero-dark  { opacity: 0; }
.dark .theme-hero-light { opacity: 0; }
.dark .theme-hero-dark  { opacity: 1; }

/* Same theme-aware swap for inline product mocks (feature pages) */
.theme-mock-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  transition: opacity 200ms ease;
}
.theme-mock-light { opacity: 1; }
.theme-mock-dark  { opacity: 0; }
.dark .theme-mock-light { opacity: 0; }
.dark .theme-mock-dark  { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  font-family: 'Plus Jakarta Sans', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  background: hsl(var(--background));
  color: hsl(var(--foreground));
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0;
}

.text-gradient {
  background: linear-gradient(135deg, hsl(var(--navy)) 0%, hsl(var(--blue)) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.dark .text-gradient {
  background: linear-gradient(135deg, hsl(210 33% 95%) 0%, hsl(207 55% 65%) 100%);
  -webkit-background-clip: text;
  background-clip: text;
}

.bg-gradient-accent {
  background: linear-gradient(135deg, hsl(var(--navy)) 0%, hsl(var(--blue)) 100%);
}

/* In dark mode the default gradient (navy → blue) blends into the dark
   page background, making CTAs look washed out. Lift both stops to keep
   the button visually punchy on a dark surface while staying on-brand.
   White text rides this fill, so the end stop sits at L=36%: white 5.25:1,
   white at 90% 4.59:1 (3.59:1 at L=45%; scripts/test_a11y_css.py). */
.dark .bg-gradient-accent {
  background: linear-gradient(135deg, hsl(214 60% 32%) 0%, hsl(199 70% 36%) 100%);
}

.bg-gradient-accent-light {
  background: linear-gradient(135deg, hsla(var(--navy) / 0.05) 0%, hsla(var(--blue) / 0.05) 100%);
}

.dark .bg-gradient-accent-light {
  background: linear-gradient(135deg, hsla(var(--blue) / 0.08) 0%, hsla(var(--navy) / 0.08) 100%);
}

/* ── Display headlines (Docket 76fd2873) ─────────────────────────────────────
   `balance` evens the lines of a heading so it stops ending on one stranded
   word ("…Stop Paying to Sit Through / It."). The owner's sentence-per-line
   <br> convention (websites/CLAUDE.md, 2026-08-21) stays: each sentence still
   starts its own line. Word spacing is opened on h1 because `tracking-tight`
   (-0.025em) at 60–72px shrinks the space glyph too, so Plus Jakarta Sans 800
   ran words together ("UnlimitedCaseStudies") while every period kept its own
   sidebearing ("Studies ."). Measured across all 27 home heroes at 1440, 1280,
   1024 and 390: one-word lines fell from 44 to 25; the rest are per-page copy.
   scripts/test_display_type.py pins both rules. */
h1,
h2 {
  text-wrap: balance;
}
h1 {
  word-spacing: 0.04em;
}

/* The header is sticky (partials/_header.jinja), so the phone menu can now be
   opened from anywhere on a page, with body scroll locked while it is open.
   Let a long menu scroll inside itself instead of running off the screen. */
#mobile-menu {
  max-height: calc(100vh - 4rem);
  max-height: calc(100dvh - 4rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Accessible focus ring. `summary` is listed so FAQ and transcript disclosures
   stop falling back to Chrome's #005FCC ring (2.36-2.68:1 on dark surfaces). */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
summary:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 2px;
  border-radius: 4px;
}
/* A summary filling an overflow-hidden card (every FAQ accordion) would lose
   that ring whole, since a positive offset draws it outside the card's clip;
   draw it just inside, on the card's rounded-xl inner corner. */
.overflow-hidden > summary:focus-visible,
.overflow-hidden > details > summary:focus-visible {
  outline-offset: -2px;
  border-radius: 0.6875rem;
}
/* On bands dark in both themes (photo heroes, the accent gradient) the accent
   ring measures 2.45:1, so it turns white (13.7:1 on navy, 5.25:1 on the
   gradient). (0,2,0) and later, so it beats every site focus-ring selector. */
.hero-bleed :focus-visible,
.bg-gradient-accent :focus-visible {
  outline-color: #fff;
}

/* Keyboard focus clears the sticky header (WCAG 2.4.11): a focused element
   keeps 8rem above it when scrolled into view, the clearance the fleet's
   anchors take (`scroll-mt-32`), so Shift+Tab no longer lands under the header
   or a jump-nav. Not `html { scroll-padding-top }`: measured in Chromium, that
   moved every anchor 80px below its own scroll-mt-32 and jumped the page 431px
   on Tab between two header links. Focus inside a sticky band (the header and
   phone menu, a jump-nav, the tour's chapters, the iOS story) takes no margin:
   the band never scrolls away from itself, so the browser would chase it. */
:focus-visible {
  scroll-margin-top: 8rem;
}
.sticky :focus-visible,
.product-chapters :focus-visible,
.story-sticky :focus-visible {
  scroll-margin-top: 0;
}

/* Skip link */
/* WCAG-standard visually-hidden pattern. The older left: -9999px trick
   could cause horizontal overflow on some mobile browsers and surface
   the link as a stray underlined "Skip to main content" line. */
.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* Focused, it shows: every clipping property of the hidden state is reset.
   Until 2026-10-07 the clip, clip-path and overflow stayed, so the first Tab
   focused an invisible link. The fill is the light accent in both themes
   (white 5.59:1; dark mode's --accent gave 2.70:1). */
.skip-link:focus,
.skip-link:focus-visible {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 9999;
  width: auto;
  height: auto;
  margin: 0;
  padding: 8px 16px;
  overflow: visible;
  clip: auto;
  clip-path: none;
  background: hsl(207 55% 40%);
  color: #fff;
  border-radius: 4px;
  font-weight: 600;
  outline: 2px solid hsl(var(--foreground));
  outline-offset: 2px;
}

/* Scrollbar */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: hsl(var(--muted));
}
::-webkit-scrollbar-thumb {
  background: hsl(var(--muted-foreground) / 0.4);
  border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
  background: hsl(var(--muted-foreground) / 0.6);
}

/* Accessible checkmark icon — meets WCAG 1.4.11 non-text contrast ≥3:1 on both
   light (--card: white) and dark card surfaces. #16a34a fails light cards;
   hsl(160 84% 29%) is the --success token and passes 3:1 on white. */
.check-icon { color: hsl(var(--success)); }
.dark .check-icon { color: hsl(160 70% 52%); }

/* Form error state — dark-mode-aware color for WCAG 1.4.3 (≥4.5:1) */
.form-error { color: hsl(var(--destructive)); }
.dark .form-error { color: hsl(0 72% 68%); }

/* Card helper */
.as-card {
  background: hsl(var(--card));
  color: hsl(var(--card-foreground));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
}

/* Course carousel — hide scrollbar on webkit + firefox */
.course-carousel { scrollbar-width: thin; }
.course-carousel::-webkit-scrollbar { height: 8px; }
.course-carousel::-webkit-scrollbar-thumb { background: hsl(var(--border)); border-radius: 9999px; }
.course-carousel::-webkit-scrollbar-track { background: transparent; }

/* Lock page scroll while the mobile menu is open. The class is toggled by
   main.js when the hamburger is tapped. `overflow: hidden` goes on <html> ONLY:
   on <body> as well, the body stops propagating its overflow to the viewport
   and becomes a scroll container of its own, so the sticky header pinned to
   the body instead of the screen. Opened while scrolled, the menu then sat
   above the viewport and focusing its first link yanked the page upward. */
html.mobile-menu-open {
  overflow: hidden;
}
body.mobile-menu-open {
  touch-action: none;
}

/* ── Accessible nav dropdowns (JS-driven disclosure) ─────────────────────────
   _header.jinja's four desktop dropdowns (Features, Products, per-item
   submenus, language picker) are `<div class="relative group">` wrapping a
   `<button aria-haspopup="true" aria-expanded aria-controls>` and a sibling
   `.dropdown-panel`. Pointer users keep the plain CSS reveal below
   (group-hover, untouched). Keyboard and click users are driven by
   main.js's wireDropdowns(), which toggles a `data-open="true"` attribute on
   the `.group` wrapper in lockstep with `aria-expanded` — this rule is what
   CSS honors for that state.

   Deliberately NOT keyed off `:focus-within`: an element with
   `visibility: hidden` cannot receive focus, Safari does not focus a
   <button> on click (so `:focus-within` never fires there at all), and a
   panel that reveals itself as a side-effect of focus merely arriving —
   rather than a deliberate click/Enter/Space/ArrowDown — leaves
   `aria-expanded` out of sync with what's actually on screen. JS owns both
   in lockstep instead. Specificity (0,3,0) beats the plain `.opacity-0` /
   `.invisible` utility classes (0,1,0) regardless of stylesheet order, so no
   `!important` is needed. */
.group[data-open="true"] > .dropdown-panel {
  opacity: 1;
  visibility: visible;
}

/* ── Responsive comparison table (partials/_comparison_table.jinja) ─────────
   <640px: stacked cards, one per column (plan/competitor) — the PRIMARY
   view, since most marketing-site traffic is mobile and a stacked card
   reads better on a phone than any horizontally-scrolled table.
   >=640px: the familiar table, wrapped for accessible horizontal scroll
   (role=region + aria-label + tabindex=0, WAI-ARIA APG "scrollable region")
   with a sticky first column and a visible scroll cue. All colors ride the
   existing hsl(var(--...)) custom properties, so both themes are handled by
   the same rules with no `.dark` overrides needed here. */

/* -- mobile stacked cards -- */
/* `sm:hidden` on the markup is NOT sufficient here and never was: style.css
   loads AFTER tailwind-compiled.css, and `.as-compare__cards{display:flex}`
   and `.sm\:hidden{display:none}` are both single-class selectors, so the
   later sheet wins and the cards stayed `display:flex` at every width. The
   result was BOTH renders on screen at once — the stacked cards and then the
   full table underneath them — on every comparison table in the fleet.
   The breakpoint therefore lives here, next to the rule it has to beat. */
.as-compare__cards {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
@media (min-width: 640px) {
  .as-compare__cards { display: none; }
}
.as-compare__card {
  background: hsl(var(--card));
  color: hsl(var(--card-foreground));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  overflow: hidden;
}
.as-compare__card--highlight {
  border: 2px solid hsl(var(--accent));
}
.as-compare__card-head {
  padding: 0.875rem 1.125rem;
  font-weight: 700;
  font-size: 1rem;
  background: hsl(var(--muted) / 0.5);
  border-bottom: 1px solid hsl(var(--border));
}
.as-compare__card--highlight .as-compare__card-head {
  background: hsl(var(--accent) / 0.12);
  color: hsl(var(--accent));
}
.as-compare__card-body {
  margin: 0;
  padding: 0.25rem 1.125rem 0.5rem;
}
.as-compare__card-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.625rem 0;
  border-bottom: 1px solid hsl(var(--border) / 0.6);
  font-size: 0.875rem;
}
.as-compare__card-row:last-child { border-bottom: 0; }
.as-compare__card-row dt {
  margin: 0;
  color: hsl(var(--muted-foreground));
  font-weight: 500;
}
.as-compare__card-row dd {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-weight: 600;
  color: hsl(var(--foreground));
  text-align: right;
}

/* -- desktop horizontal-scroll table -- */
.as-compare__scroll-wrap { position: relative; }
.as-compare__scroll {
  overflow-x: auto;
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  -webkit-overflow-scrolling: touch;
}
.as-compare__scroll:focus-visible {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 2px;
}
.as-compare__table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 0.875rem;
}
/* Cells WRAP. `white-space: nowrap` here was what forced the horizontal
   scroll: a cell like "A separate purchase; often priced near a full CPA
   course" became one unbreakable line, so a 3-column table needed 979px
   inside an 830px container. Wrapping keeps every word and removes the
   scroll; `balance` stops a 2-line cell ending in one orphaned word. */
.as-compare__table th,
.as-compare__table td {
  padding: 0.875rem 1rem;
  text-align: center;
  white-space: normal;
  text-wrap: balance;
  border-bottom: 1px solid hsl(var(--border));
}
.as-compare__table tbody tr:last-child th,
.as-compare__table tbody tr:last-child td {
  border-bottom: 0;
}
.as-compare__table thead th {
  font-weight: 700;
  color: hsl(var(--foreground));
  background: hsl(var(--muted) / 0.5);
}
.as-compare__table tbody th {
  text-align: left;
  font-weight: 500;
  color: hsl(var(--muted-foreground));
  white-space: normal;
}
.as-compare__col--highlight {
  background: hsl(var(--accent) / 0.06);
  color: hsl(var(--accent));
  font-weight: 700;
}
.as-compare__table td.as-compare__col--highlight {
  color: hsl(var(--foreground));
}
/* Sticky first column: stays in place while the row scrolls under it. Needs
   its own opaque background (matching whichever row band it's in) so
   scrolled content doesn't show through underneath it. */
.as-compare__sticky-col {
  position: sticky;
  left: 0;
  z-index: 1;
  background: hsl(var(--card));
  box-shadow: 1px 0 0 hsl(var(--border));
  text-align: left;
}
.as-compare__table thead .as-compare__sticky-col {
  background: hsl(var(--muted));
  z-index: 2;
}
/* Accessible check/x glyphs: green check reuses --success (already tuned to
   pass 3:1 non-text contrast in both themes elsewhere in this file); the
   "not included" mark uses muted-foreground rather than red — this is an
   absence, not an error. */
.as-compare__icon { display: inline-block; height: 1rem; width: 1rem; vertical-align: -2px; }
.as-compare__icon--yes { color: hsl(var(--success)); }
.as-compare__icon--no { color: hsl(var(--muted-foreground)); }

/* Visible scroll cue: a fading edge + chevron hinting there's more table off
   to the right. Purely decorative (aria-hidden), absolutely positioned on
   the non-scrolling wrapper so it stays fixed at the visible right edge
   regardless of scroll position (a static affordance, not a
   scroll-position-aware one — deliberately simple, no JS dependency). */
/* Shown only when the table really does overflow. Until 2026-08-21 this was
   a static, always-on affordance ("deliberately simple, no JS dependency") —
   which was defensible while every cell was nowrap and the table therefore
   ALWAYS scrolled. Now that cells wrap it usually does not, and a permanent
   "there's more →" arrow pointing at nothing is worse than the JS. The
   toggle is in main.js; with JS off the cue simply never appears, which is
   the safe direction for a decorative hint. */
.as-compare__scroll-cue { display: none; }
.as-compare__scroll-wrap--scrollable .as-compare__scroll-cue {
  position: absolute;
  top: 1px;
  right: 1px;
  bottom: 1px;
  width: 2.75rem;
  pointer-events: none;
  border-radius: 0 var(--radius) var(--radius) 0;
  background: linear-gradient(to right, transparent, hsl(var(--card)) 65%);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 0.5rem;
}
.as-compare__scroll-cue svg {
  width: 1.1rem;
  height: 1.1rem;
  color: hsl(var(--accent));
  opacity: 0.85;
}

/* ── Sticky mobile CTA bar (partials/_sticky_cta.jinja) ──────────────────────
   Fixed to the bottom of the viewport below `md` (768px) only — desktop
   already shows the persistent header CTA. Respects the iOS home-indicator
   safe area via env(safe-area-inset-bottom) so the button never sits under
   it. `.as-sticky-cta-spacer` is the matching bottom-padding helper pages
   must add to their last content block — see the partial's own doc comment
   for why (short pages would otherwise have the bar cover real content). */
.as-sticky-cta {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: hsl(var(--card));
  border-top: 1px solid hsl(var(--border));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08);
}
.dark .as-sticky-cta {
  box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35);
}
.as-sticky-cta__inner {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  max-width: 640px;
  margin: 0 auto;
  padding: 0.625rem 1rem;
}
.as-sticky-cta__secondary {
  font-size: 0.75rem;
  line-height: 1.3;
  color: #5A6577; /* Renkara Slate — light-mode muted text, AA on white */
  flex: 1 1 auto;
  min-width: 0;
}
.dark .as-sticky-cta__secondary {
  color: #94A3B8; /* slate-400 — dark-mode muted text, AA on dark surfaces */
}
.as-sticky-cta__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px; /* WCAG 2.5.5 target size */
  padding: 0 1.25rem;
  border-radius: 9999px;
  font-size: 0.875rem;
  font-weight: 600;
  white-space: nowrap;
  flex-shrink: 0;
}
/* Bottom-padding helper for page content sharing the viewport with the bar.
   Collapses to zero at the same breakpoint the bar itself disappears at, so
   it is always safe to leave the class on regardless of viewport width. */
.as-sticky-cta-spacer {
  padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
}
@media (min-width: 768px) {
  .as-sticky-cta-spacer { padding-bottom: 0; }
}

/* ── Legal document body (partials/_legal_privacy_body.jinja,
   _legal_terms_body.jinja) ──────────────────────────────────────────────────
   Both partials render long-form HTML (h2/h3/p/ul/ol/table) inside
   `.legal-body`, and the privacy body additionally wraps its data tables in
   `.table-wrapper` — neither class had a CSS definition anywhere in this
   overlay. Consequence: Tailwind's Preflight reset strips all default
   heading/list/paragraph/table styling, so every element rendered
   identically (same size, no spacing, no bullets, no borders), and the
   wide multi-column tables had no horizontal-scroll handling at all on
   narrow viewports. The template HTML itself is untouched here — these
   classes already existed on every element that needs them. */

/* -- prose typography -- */
.legal-body h2 {
  font-size: 1.5rem;
  font-weight: 800;
  color: hsl(var(--foreground));
  margin: 0 0 0.75rem;
  line-height: 1.3;
}
.legal-body h3 {
  font-size: 1.125rem;
  font-weight: 700;
  color: hsl(var(--foreground));
  margin: 1.5rem 0 0.5rem;
  line-height: 1.4;
}
.legal-body p { margin: 0 0 1rem; }
.legal-body ul,
.legal-body ol {
  margin: 0 0 1rem;
  padding-left: 1.5rem;
}
.legal-body ul { list-style: disc; }
.legal-body ol { list-style: decimal; }
.legal-body li { margin: 0 0 0.5rem; }
.legal-body li > ul,
.legal-body li > ol { margin: 0.5rem 0 0; }
.legal-body strong { color: hsl(var(--foreground)); font-weight: 700; }
.legal-body em { font-style: italic; }
/* `anywhere` lets a long URL written as link text break inside the column: at
   320px the terms page's ODR address pushed the page sideways (WCAG 1.4.10). */
.legal-body a {
  color: hsl(var(--accent));
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;
}
.legal-body a:hover { opacity: 0.85; }
.legal-body hr {
  border: 0;
  border-top: 1px solid hsl(var(--border));
  margin: 2rem 0;
}

/* -- data tables: accessible horizontal scroll (same technique as
   .as-compare__scroll — role=region + aria-label + tabindex=0 are set in
   the template on each .table-wrapper; this is the matching CSS half) -- */
.legal-body .table-wrapper {
  overflow-x: auto;
  margin: 0 0 1.5rem;
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  -webkit-overflow-scrolling: touch;
}
.legal-body .table-wrapper:focus-visible {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 2px;
}
.legal-body table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 0.8125rem;
}
.legal-body th,
.legal-body td {
  padding: 0.75rem 1rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid hsl(var(--border));
}
.legal-body tbody tr:last-child th,
.legal-body tbody tr:last-child td {
  border-bottom: 0;
}
.legal-body thead th {
  font-weight: 700;
  color: hsl(var(--foreground));
  background: hsl(var(--muted) / 0.5);
  white-space: nowrap;
}
.legal-body tbody td:first-child { white-space: nowrap; }

/* Horizontal pill / tab strips (FAQ section tabs, page jump navs).
   Centered when the pills fit, scrolling from the start when they do not.
   `safe center` is the load-bearing half: plain `center` in a scroll
   container pushes the leading pills into negative space where they cannot
   be scrolled to. The plain declaration above it is the fallback for
   browsers without `safe` (they get today's left alignment, not a broken
   strip). Owner directive 2026-08-21: FAQ tabs are centered. */
.as-strip-center { justify-content: center; justify-content: safe center; }

/* ── Header dropdown panels ──────────────────────────────────────────────────
   OPEN fades; CLOSE is instant.

   The panels used to carry Tailwind's `transition-all duration-150`, which
   includes `visibility` — and `visibility` is a discrete-animated property, so
   the switch to `hidden` is deferred to the END of the transition. A panel
   therefore stayed fully visible for 150ms after the cursor left it. Sweeping
   across the nav bar left two, then three, panels stacked on screen at once
   (measured: 2 panels visible across four consecutive 40ms samples). Only
   `opacity` is transitioned now, so leaving a menu hides it on the same frame.

   Two things open a panel: the pointer (the panel's group-hover classes) and
   main.js's wireDropdowns(), whose click, Enter, Space and ArrowDown set
   [data-open] in step with aria-expanded (the rule under "Accessible nav
   dropdowns"). Focus alone opens nothing: until 2026-10-07 a
   `.group:focus-within` rule here did, so a panel stood open while
   aria-expanded said "false", Escape could not close it, and its links stayed
   Tab stops. Escape also hides a hover-opened panel (WCAG 1.4.13): main.js
   marks the hovered .group [data-dismissed] until the pointer leaves it. */
.dropdown-panel { transition: opacity 150ms ease; }
.group[data-dismissed]:not([data-open]) > .dropdown-panel { opacity: 0 !important; visibility: hidden !important; }

/* ── Home-page promo film (partials/_promo_video.jinja) ─────────────────────
   The film is dark navy in both themes, so its frame is too: a 16:9 card on
   the film's own navy (no flash of page color while the poster loads), a
   soft accent glow beneath it, and one large play button over the open
   network art on the right of the owner's poster, clear of the tagline and
   the wordmark. The section clips the glow so it never scrolls sideways. */
.promo-video-section { overflow: hidden; }
.promo-video { position: relative; isolation: isolate; margin: 0; }
.promo-video::before {
  content: "";
  position: absolute; inset: 8% 2% -6%; z-index: -1;
  border-radius: 2rem;
  background: radial-gradient(closest-side, hsl(var(--accent) / 0.45), transparent);
  filter: blur(48px);
  pointer-events: none;
}
.dark .promo-video::before { background: radial-gradient(closest-side, hsl(var(--accent) / 0.3), transparent); }
.promo-video-frame {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  border-radius: 1.25rem;
  background: #0b1628;
  box-shadow: 0 0 0 1px hsl(var(--border)), 0 32px 64px -24px rgb(11 22 40 / 0.55), 0 12px 24px -12px rgb(11 22 40 / 0.35);
}
.dark .promo-video-frame { box-shadow: 0 0 0 1px hsl(0 0% 100% / 0.1), 0 32px 64px -24px rgb(0 0 0 / 0.7); }
.promo-video-media { display: block; width: 100%; height: 100%; object-fit: cover; background: #0b1628; }
.promo-video-play {
  position: absolute; inset: 0; display: block; width: 100%; margin: 0; padding: 0;
  border: 0; cursor: pointer;
  background: linear-gradient(to right, transparent 55%, rgb(3 7 18 / 0.28));
  -webkit-tap-highlight-color: transparent;
}
.promo-video-play[hidden] { display: none; }
.promo-video-play:focus-visible { outline: none; }
.promo-video-play-icon {
  position: absolute; left: var(--play-x, 84%); top: var(--play-y, 50%);
  width: clamp(3.75rem, 9vw, 6rem); aspect-ratio: 1;
  display: grid; place-items: center;
  border-radius: 9999px; color: #fff;
  background: hsl(0 0% 100% / 0.14);
  border: 1px solid hsl(0 0% 100% / 0.45);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  box-shadow: 0 0 0 10px hsl(207 76% 65% / 0.14), 0 16px 40px -10px hsl(207 76% 65% / 0.6);
  transform: translate(-50%, -50%);
  transition: transform 0.25s ease, background-color 0.25s ease, box-shadow 0.25s ease;
}
.promo-video-play-icon svg { width: 38%; height: 38%; margin-left: 8%; fill: currentColor; }
/* A slow ripple that invites the click, twice and then still: 4.8s of motion,
   under WCAG 2.2.2's five seconds. Reduced motion turns it off (end of file). */
.promo-video-play-icon::after {
  content: ""; position: absolute; inset: -1px; border-radius: inherit;
  border: 1px solid hsl(0 0% 100% / 0.5);
  animation: promo-video-ripple 2.4s ease-out 2;
}
@keyframes promo-video-ripple {
  from { transform: scale(1); opacity: 0.8; }
  to { transform: scale(1.55); opacity: 0; }
}
.promo-video-play:hover .promo-video-play-icon,
.promo-video-play:focus-visible .promo-video-play-icon {
  transform: translate(-50%, -50%) scale(1.08);
  background: hsl(0 0% 100% / 0.24);
  box-shadow: 0 0 0 14px hsl(207 76% 65% / 0.18), 0 20px 48px -10px hsl(207 76% 65% / 0.7);
}
.promo-video-play:focus-visible .promo-video-play-icon { outline: 3px solid #fff; outline-offset: 5px; }
.promo-video-chip {
  position: absolute; left: clamp(0.75rem, 2.5%, 1.25rem); bottom: clamp(0.75rem, 3.5%, 1.25rem);
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.375rem 0.8rem; border-radius: 9999px;
  font-size: 0.8125rem; font-weight: 600; line-height: 1.2; color: #fff;
  background: rgb(3 7 18 / 0.6); border: 1px solid hsl(0 0% 100% / 0.18);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.promo-video-chip-dot { width: 3px; height: 3px; border-radius: 9999px; background: currentColor; opacity: 0.7; }
.promo-video-transcript { margin: 1.75rem auto 0; max-width: 48rem; font-size: 0.875rem; color: hsl(var(--muted-foreground)); }
.promo-video-transcript summary {
  width: fit-content; margin: 0 auto; cursor: pointer; font-weight: 500;
  color: hsl(var(--foreground));
  text-decoration: underline; text-decoration-color: hsl(var(--border)); text-underline-offset: 4px;
}
.promo-video-transcript summary:hover { color: hsl(var(--accent)); }
.promo-video-transcript ol { margin-top: 1rem; padding-left: 1.25rem; list-style: decimal; display: grid; gap: 0.375rem; }
.promo-video-transcript p { margin-top: 0.75rem; font-size: 0.75rem; }
/* The share button under a film (film-share.js). The status line keeps its
   height, so "Link copied" never shifts the transcript below it. The menu opens
   upward, over the film: .promo-video-section clips its overflow (the frame's
   glow), and a menu opening down was cut off at the section's edge. */
.film-share { position: relative; display: flex; flex-direction: column; align-items: center; margin-top: 1.5rem; }
.film-share[hidden] { display: none; }
.film-share + .promo-video-transcript { margin-top: 0.75rem; }
.film-share-button {
  display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem;
  border-radius: 9999px; border: 1px solid hsl(var(--border));
  background: hsl(var(--card)); color: hsl(var(--foreground));
  font-size: 0.875rem; font-weight: 500; line-height: 1.25rem; cursor: pointer;
  transition: border-color 150ms ease, color 150ms ease;
}
.film-share-button:hover, .film-share-button[aria-expanded="true"] { border-color: hsl(var(--accent)); color: hsl(var(--accent)); }
.film-share-button:focus-visible { outline: 2px solid hsl(var(--accent)); outline-offset: 2px; }
.film-share-icon { width: 1rem; height: 1rem; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.film-share-menu {
  position: absolute; bottom: calc(100% + 0.5rem); left: 50%; transform: translateX(-50%); z-index: 20;
  min-width: 12rem; padding: 0.375rem; list-style: none;
  border-radius: 0.75rem; border: 1px solid hsl(var(--border));
  background: hsl(var(--card)); color: hsl(var(--card-foreground));
  box-shadow: 0 18px 40px -14px rgb(0 0 0 / 0.35);
}
.film-share-menu[hidden] { display: none; }
.film-share-menu a, .film-share-menu button {
  display: block; width: 100%; padding: 0.5rem 0.75rem; border-radius: 0.5rem;
  text-align: start; font-size: 0.875rem; color: inherit; text-decoration: none; cursor: pointer;
}
.film-share-menu a:hover, .film-share-menu button:hover { background: hsl(var(--muted)); }
.film-share-menu a:focus-visible, .film-share-menu button:focus-visible { background: hsl(var(--muted)); outline: 2px solid hsl(var(--accent)); outline-offset: -2px; }
.film-share-status { min-height: 1.25rem; margin-top: 0.375rem; font-size: 0.8125rem; color: hsl(var(--muted-foreground)); overflow-wrap: anywhere; }
/* A light poster (the test-prep walkthrough) takes a dark play button, and its
   frame waits on the poster's own off-white. The chip can sit on the right,
   off a poster's fine print (the Certs walkthrough). A narrated film's
   transcript reads as body text. */
.promo-video--light-poster .promo-video-frame,
.promo-video--light-poster .promo-video-media { background: #f3f5f1; }
.promo-video--light-poster .promo-video-play { background: none; }
.promo-video--light-poster .promo-video-play-icon {
  background: rgb(11 22 40 / 0.78);
  border-color: rgb(11 22 40 / 0.18);
  box-shadow: 0 0 0 10px rgb(11 22 40 / 0.08), 0 16px 40px -10px rgb(11 22 40 / 0.45);
}
.promo-video--light-poster .promo-video-play-icon::after { border-color: rgb(11 22 40 / 0.35); }
.promo-video--light-poster .promo-video-play:hover .promo-video-play-icon,
.promo-video--light-poster .promo-video-play:focus-visible .promo-video-play-icon {
  background: rgb(11 22 40 / 0.9);
  box-shadow: 0 0 0 14px rgb(11 22 40 / 0.1), 0 20px 48px -10px rgb(11 22 40 / 0.55);
}
.promo-video--light-poster .promo-video-play:focus-visible .promo-video-play-icon { outline-color: #0b1628; }
.promo-video-chip--right { left: auto; right: clamp(0.75rem, 2.5%, 1.25rem); }
.promo-video-narration p { margin-top: 0.875rem; font-size: 0.9375rem; line-height: 1.7; }
@media (max-width: 639px) {
  .promo-video-frame { border-radius: 0.875rem; }
  .promo-video-chip { font-size: 0.75rem; padding: 0.25rem 0.625rem; }
}
@media (prefers-reduced-motion: reduce) {
  .promo-video-play-icon, .promo-video-play-icon::after { transition: none; animation: none; }
}
