/* ============================================================
   KAIS Homepage — Dark Sophisticated Theme
   ============================================================ */

/* 1. Custom Properties
   ------------------------------------------------------------ */
:root {
  --brand: #6aa8ff;
  --brand-2: #9b6dff;
  /* Solid, darker accent for white-on-accent TEXT. The --gradient-cta
     (brand -> brand-2) is too light for 4.5:1 white text (2.4-3.5:1);
     white on #7c3aed measures 5.7:1 (WCAG 2.2 AA, small text). */
  --brand-accent-strong: #7c3aed;
  --ok: #2fd37a;

  --kais-green: #2fd37a;
  --kais-blue: #6aa8ff;
  --kais-purple: #9b6dff;
  --kais-orange: #FF9600;
  --kais-red: #FF4B4B;
  --kais-yellow: #FFC800;

  --bg-page: #0b1020;
  --bg-alt: #0e1429;
  --bg-card: #121939;
  --text-heading: #e7e9f3;
  --text-body: #b9bfd3;
  --text-muted: #7a83a0;
  /* Stronger muted for the SMALLEST legal/disclaimer copy: --text-muted on
     --bg-card is 4.56:1 (a razor-thin AA pass at <= 0.72rem); this measures
     5.6:1 for comfortable margin on small text. */
  --text-muted-strong: #8a93b2;
  --border-light: rgba(255, 255, 255, .1);
  --border-card: rgba(255, 255, 255, .08);

  --gradient-hero: radial-gradient(1200px 600px at 80% -10%, rgba(155, 109, 255, .25), transparent),
                   radial-gradient(900px 500px at -10% 10%, rgba(106, 168, 255, .25), transparent),
                   var(--bg-page);
  --gradient-cta: linear-gradient(90deg, var(--brand), var(--brand-2));

  --shadow-card: 0 10px 30px rgba(0, 0, 0, .35);
  --shadow-card-hover: 0 25px 60px rgba(31, 45, 92, .45);
  --shadow-btn: 0 4px 15px rgba(106, 168, 255, .3);

  /* ---- Design-system layer (2026-07 visual pass) ----------------
     Depth, lighting and motion tokens. Everything below consumes
     these rather than re-declaring raw rgba(), so the whole page can
     be re-lit from one place. */

  /* Easing. --ease-out is the "settle" curve (no overshoot) used for
     surfaces; --ease-spring keeps the existing playful overshoot for
     small elements that can afford to bounce. */
  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --ease-spring: cubic-bezier(.175, .885, .32, 1.275);

  /* Strokes. Two weights so a card can carry a soft edge AND a
     brighter top-light seam without inventing new rgba values. */
  --stroke-soft: rgba(255, 255, 255, .09);
  --stroke-strong: rgba(255, 255, 255, .16);

  /* The single most important trick in this pass: a 1px inner top
     highlight. Real materials catch light on their top edge; without
     it every dark card reads as a flat hole punched in the page. */
  --inner-top-light: inset 0 1px 0 rgba(255, 255, 255, .11);

  /* Coloured elevation. Dark UIs read as premium when the shadow
     carries the accent hue rather than pure black. */
  --glow-blue: 0 22px 60px -26px rgba(106, 168, 255, .75);
  --glow-purple: 0 22px 60px -26px rgba(155, 109, 255, .75);
  --glow-orange: 0 22px 60px -26px rgba(255, 150, 0, .55);

  /* Fine dot grid used as a texture wash on the hero + final CTA.
     Pure CSS — no image request, no data-URI, nothing to cache. */
  --dot-grid: radial-gradient(circle at center, rgba(255, 255, 255, .07) 1px, transparent 1.2px);
  --dot-grid-size: 24px 24px;

  /* Diagonal specular sweep reused by buttons, the price card and
     the compare bars. */
  --sheen: linear-gradient(115deg,
            transparent 32%,
            rgba(255, 255, 255, .16) 47%,
            rgba(255, 255, 255, .06) 54%,
            transparent 68%);

  /* Consistent focus ring for the whole page. */
  --focus-ring: 0 0 0 3px rgba(11, 16, 32, .9), 0 0 0 6px var(--brand);

  --radius-card: 20px;
  --radius-btn: 999px;
  --radius-pill: 999px;

  --card-radius: 16px;
  --card-radius-lg: 20px;
  --card-padding-y: 1.5rem;
  --card-padding-x: clamp(1rem, 2vw, 1.5rem);

  --container: 1140px;
  --font: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}

/* 1b. Light palette
   ------------------------------------------------------------
   The homepage is one of the "dark islands" theme.css section D2 names:
   base.html's chrome is themed, this page's palette was not, so D2
   re-declares the dark values on `.kais-home` to stop a half-flip. This
   section is the migration D2's header calls "a deletion" — once it is
   here, `.kais-home` can come out of that selector list.

   TWO BLOCKS, ONE PAYLOAD, SPELLED TWICE.
   `:root[data-theme="light"]` is the explicit opt-in theme-init.js
   stamps; the `prefers-color-scheme` block below is `auto`, and it MUST
   stay scoped to `:root[data-theme="auto"]`. An unscoped `:root` there
   repaints this page for a light-OS visitor who explicitly chose DARK,
   which is a regression, not a feature (theme.css THEMING banner).
   The values cannot be shared through an indirection — the contrast
   sweep in tests/test_static/test_theme_pages_a.py resolves them to
   opaque hex — so KEEP THE TWO BLOCKS IN SYNC. A test does too.

   BOTH selectors are load-bearing. `:root[data-theme="light"]` (0,2,0)
   beats this file's own bare `:root` (0,1,0); `:root[data-theme="light"]
   .kais-home` (0,3,0) is what TIES theme.css D2 and wins on source
   order, because page sheets are linked from `{% block extra_head %}`,
   i.e. after theme.css. Drop the second selector and the whole palette
   below is inert. `@media` adds ZERO specificity, so the auto copy needs
   exactly the same treatment.

   Dark mode is untouched: every declaration here is ADDITIVE, and the
   tokens that theme literals are undefined in dark so their consumers
   fall back to the literal that was always there.
   ------------------------------------------------------------ */
:root[data-theme="light"],
:root[data-theme="light"] .kais-home {
  color-scheme: light;
  accent-color: #2563eb;

  /* -- shared surface vocabulary -------------------------------------
     The same token NAMES tailwind-main.css, theme.css, dashboard.css and
     note.css use for the same roles, so one block re-themes the page. */
  --bg-page: #f1f5f9;
  --bg-primary: #f1f5f9;
  --bg-alt: #e8eef6;
  --bg-card: #ffffff;
  --surface: #ffffff;
  --hover-bg: #e2e8f0;

  --text-heading: #0f172a;
  --text-dark: #0f172a;
  --text-body: #334155;
  --text-light: #334155;
  --text-muted: #475569;
  --text-muted-strong: #47536b;
  --muted-text: rgba(15, 23, 42, .85);

  /* The white-alpha veils invert to black-alpha: a rgba(255,255,255,.1)
     hairline is invisible on a white card. */
  --border-light: rgba(15, 23, 42, .12);
  --border-card: rgba(15, 23, 42, .10);
  --border-gray: rgba(15, 23, 42, .12);
  --stroke-soft: rgba(15, 23, 42, .09);
  --stroke-strong: rgba(15, 23, 42, .16);

  --accent-blue: #2563eb;
  --link-visited: #7c3aed;

  --shadow-card: 0 10px 30px rgba(15, 23, 42, .10);
  --shadow-card-hover: 0 25px 60px rgba(31, 45, 92, .18);
  --shadow-btn: 0 4px 15px rgba(29, 78, 216, .25);
  --inner-top-light: inset 0 1px 0 rgba(15, 23, 42, .04);
  --dot-grid: radial-gradient(circle at center, rgba(15, 23, 42, .07) 1px, transparent 1.2px);
  --focus-ring: 0 0 0 3px #ffffff, 0 0 0 6px #1d4ed8;
  --sheen: linear-gradient(115deg,
            transparent 32%,
            rgba(15, 23, 42, .07) 47%,
            rgba(15, 23, 42, .03) 54%,
            transparent 68%);
  --glow-blue: 0 22px 60px -26px rgba(29, 78, 216, .35);
  --glow-purple: 0 22px 60px -26px rgba(109, 40, 217, .35);
  --glow-orange: 0 22px 60px -26px rgba(154, 52, 18, .30);
  --gradient-hero: radial-gradient(1200px 600px at 80% -10%, rgba(155, 109, 255, .10), transparent),
                   radial-gradient(900px 500px at -10% 10%, rgba(106, 168, 255, .12), transparent),
                   var(--bg-page);

  /* The `--kais-*` role tokens are NOT declared here. theme.css owns them
     (A1 dark / A2 explicit-light / A3 auto-light) and its section B is
     what routes this page's `.text-white`, `.text-gray-300|400` and
     `.bg-gray-800|900` utilities through them. This island is no longer
     listed in theme.css section D2, so the light values reach it by plain
     inheritance from `:root` and re-stating them here would only be a
     second copy of somebody else's value to keep in step.
     If this island is ever put BACK into D2 — which would mean "this page
     is not migrated, keep it dark" — D2's (0,3,0) beats that inheritance
     and the page needs its own copies again;
     test_theme_pages_a.py::test_island_block_covers_every_token_theme_css_re_declares
     fires exactly in that case. */

  /* Accent INK — the six `--*-ink` tokens are declared ONCE, in theme.css
     sections A2/A3, and reset in D2 so a still-dark island cannot inherit
     light ink onto a dark surface. They are NOT re-declared here: two
     copies of one value have already drifted in this repo. Consumers stay
     `var(--accent-ink, var(--brand))`, so dark mode still falls back to
     the brand literal by construction.
     tests/test_static/test_theme_pages_a.py pins theme.css as the source,
     so a rename there fails a test instead of silently un-inking six
     pages. */
  /* The brand gradient used as INK, for the `background-clip: text` sites
     only. --gradient-cta itself is NOT flipped: it is also the resting
     background of buttons and badges whose labels are white or near-black,
     and darkening it there breaks those pairings (theme.css section C makes
     the same argument for .text-white).

     COMPOSED from the ink tokens rather than restating their hexes, so
     #1d4ed8 / #6d28d9 have exactly one source (theme.css A2/A3) and this
     cannot drift from it. MEASURED degradation, because the rule here is
     not obvious: if theme.css ever stops publishing those tokens this
     value becomes guaranteed-invalid, the OUTER
     `var(--gradient-cta-ink, var(--gradient-cta))` at each call site takes
     ITS fallback, and the text paints with the brand gradient — the
     dark-mode appearance, not blank text. Verified in Chromium rather than
     reasoned about, because the opposite outcome (invisible headings) is
     the repo's worst failure class. */
  --gradient-cta-ink: linear-gradient(90deg, var(--accent-ink), var(--accent-2-ink));
  /* White-on-CTA. The brand gradient measures 2.6:1 under white text
     (home.css says so at --brand-accent-strong), which is site debt in
     BOTH themes — but a pale blue button on a white card is where it is
     most visible. Only the BUTTON reads this token; --gradient-cta
     itself stays put so accent stripes and badges are unaffected.
     Measured white-on: 6.7:1 and 7.1:1. */
  --cta-surface: linear-gradient(90deg, #1d4ed8, #6d28d9);
  /* #school-os paints its OWN dark base under the brand radials, which
     no token could reach — MEASURED at 1.9:1 under the flipped body
     text by tests/visual/theme_contrast_check.py. This is the exact
     half-flip theme.css section D2 names. */
  --school-os-surface: radial-gradient(65% 90% at 6% -22%, rgba(29, 78, 216, .10), transparent 58%),
                       radial-gradient(60% 85% at 94% 122%, rgba(109, 40, 217, .10), transparent 58%),
                       linear-gradient(180deg, #e8eef6, #f1f5f9);
  --hero-ink: #0f172a;
  --ghost-ink: rgba(15, 23, 42, .06);
  --ghost-ink-hover: rgba(15, 23, 42, .10);

  /* Gradient TEXT (background-clip: text) — BOTH stops are read as ink,
     so the pale second stop has to darken too. */
  --highlight-green: linear-gradient(90deg, #14532d, #166534);
  --highlight-blue: linear-gradient(90deg, #1e40af, #1d4ed8);
  --highlight-purple: linear-gradient(90deg, #5b21b6, #6d28d9);
  --highlight-orange: linear-gradient(90deg, #78350f, #9a3412);

  /* -- surfaces that ship as hard literals ------------------------------
     Each consumer is `var(--token, <the exact literal that was there>)`,
     so dark mode is byte-identical BY CONSTRUCTION rather than by my
     having retyped a value correctly. */
  --card-surface: #ffffff;
  --section-alt-surface: #e8eef6;
  --hero-chip-surface: linear-gradient(180deg, rgba(29, 78, 216, .10), rgba(29, 78, 216, .04)), #ffffff;
  --hero-price-surface: linear-gradient(180deg, rgba(29, 78, 216, .08), rgba(109, 40, 217, .04) 60%), #ffffff;
  --media-surface: #e2e8f0;
  --track-surface: rgba(15, 23, 42, .10);
  --track-inset: rgba(15, 23, 42, .18);
  --cta-scrim: linear-gradient(180deg, rgba(241, 245, 249, .82) 0%, rgba(241, 245, 249, .94) 100%);
  --rule-veil: rgba(15, 23, 42, .14);
  --chip-veil: rgba(15, 23, 42, .05);
  --chip-veil-strong: rgba(15, 23, 42, .07);
  --panel-veil: rgba(15, 23, 42, .04);
}

@media (prefers-color-scheme: light) {
  :root[data-theme="auto"],
  :root[data-theme="auto"] .kais-home {
    color-scheme: light;
    accent-color: #2563eb;

    /* -- shared surface vocabulary -------------------------------------
       The same token NAMES tailwind-main.css, theme.css, dashboard.css and
       note.css use for the same roles, so one block re-themes the page. */
    --bg-page: #f1f5f9;
    --bg-primary: #f1f5f9;
    --bg-alt: #e8eef6;
    --bg-card: #ffffff;
    --surface: #ffffff;
    --hover-bg: #e2e8f0;

    --text-heading: #0f172a;
    --text-dark: #0f172a;
    --text-body: #334155;
    --text-light: #334155;
    --text-muted: #475569;
    --text-muted-strong: #47536b;
    --muted-text: rgba(15, 23, 42, .85);

    /* The white-alpha veils invert to black-alpha: a rgba(255,255,255,.1)
       hairline is invisible on a white card. */
    --border-light: rgba(15, 23, 42, .12);
    --border-card: rgba(15, 23, 42, .10);
    --border-gray: rgba(15, 23, 42, .12);
    --stroke-soft: rgba(15, 23, 42, .09);
    --stroke-strong: rgba(15, 23, 42, .16);

    --accent-blue: #2563eb;
    --link-visited: #7c3aed;

    --shadow-card: 0 10px 30px rgba(15, 23, 42, .10);
    --shadow-card-hover: 0 25px 60px rgba(31, 45, 92, .18);
    --shadow-btn: 0 4px 15px rgba(29, 78, 216, .25);
    --inner-top-light: inset 0 1px 0 rgba(15, 23, 42, .04);
    --dot-grid: radial-gradient(circle at center, rgba(15, 23, 42, .07) 1px, transparent 1.2px);
    --focus-ring: 0 0 0 3px #ffffff, 0 0 0 6px #1d4ed8;
    --sheen: linear-gradient(115deg,
              transparent 32%,
              rgba(15, 23, 42, .07) 47%,
              rgba(15, 23, 42, .03) 54%,
              transparent 68%);
    --glow-blue: 0 22px 60px -26px rgba(29, 78, 216, .35);
    --glow-purple: 0 22px 60px -26px rgba(109, 40, 217, .35);
    --glow-orange: 0 22px 60px -26px rgba(154, 52, 18, .30);
    --gradient-hero: radial-gradient(1200px 600px at 80% -10%, rgba(155, 109, 255, .10), transparent),
                     radial-gradient(900px 500px at -10% 10%, rgba(106, 168, 255, .12), transparent),
                     var(--bg-page);

    /* The `--kais-*` role tokens are NOT declared here. theme.css owns them
       (A1 dark / A2 explicit-light / A3 auto-light) and its section B is
       what routes this page's `.text-white`, `.text-gray-300|400` and
       `.bg-gray-800|900` utilities through them. This island is no longer
       listed in theme.css section D2, so the light values reach it by plain
       inheritance from `:root` and re-stating them here would only be a
       second copy of somebody else's value to keep in step.
       If this island is ever put BACK into D2 — which would mean "this page
       is not migrated, keep it dark" — D2's (0,3,0) beats that inheritance
       and the page needs its own copies again;
       test_theme_pages_a.py::test_island_block_covers_every_token_theme_css_re_declares
       fires exactly in that case. */

    /* Accent INK — the six `--*-ink` tokens are declared ONCE, in theme.css
       sections A2/A3, and reset in D2 so a still-dark island cannot inherit
       light ink onto a dark surface. They are NOT re-declared here: two
       copies of one value have already drifted in this repo. Consumers stay
       `var(--accent-ink, var(--brand))`, so dark mode still falls back to
       the brand literal by construction.
       tests/test_static/test_theme_pages_a.py pins theme.css as the source,
       so a rename there fails a test instead of silently un-inking six
       pages. */
    /* The brand gradient used as INK, for the `background-clip: text` sites
       only. --gradient-cta itself is NOT flipped: it is also the resting
       background of buttons and badges whose labels are white or near-black,
       and darkening it there breaks those pairings (theme.css section C makes
       the same argument for .text-white).

       COMPOSED from the ink tokens rather than restating their hexes, so
       #1d4ed8 / #6d28d9 have exactly one source (theme.css A2/A3) and this
       cannot drift from it. MEASURED degradation, because the rule here is
       not obvious: if theme.css ever stops publishing those tokens this
       value becomes guaranteed-invalid, the OUTER
       `var(--gradient-cta-ink, var(--gradient-cta))` at each call site takes
       ITS fallback, and the text paints with the brand gradient — the
       dark-mode appearance, not blank text. Verified in Chromium rather than
       reasoned about, because the opposite outcome (invisible headings) is
       the repo's worst failure class. */
    --gradient-cta-ink: linear-gradient(90deg, var(--accent-ink), var(--accent-2-ink));
    /* White-on-CTA. The brand gradient measures 2.6:1 under white text
       (home.css says so at --brand-accent-strong), which is site debt in
       BOTH themes — but a pale blue button on a white card is where it is
       most visible. Only the BUTTON reads this token; --gradient-cta
       itself stays put so accent stripes and badges are unaffected.
       Measured white-on: 6.7:1 and 7.1:1. */
    --cta-surface: linear-gradient(90deg, #1d4ed8, #6d28d9);
    /* #school-os paints its OWN dark base under the brand radials, which
       no token could reach — MEASURED at 1.9:1 under the flipped body
       text by tests/visual/theme_contrast_check.py. This is the exact
       half-flip theme.css section D2 names. */
    --school-os-surface: radial-gradient(65% 90% at 6% -22%, rgba(29, 78, 216, .10), transparent 58%),
                         radial-gradient(60% 85% at 94% 122%, rgba(109, 40, 217, .10), transparent 58%),
                         linear-gradient(180deg, #e8eef6, #f1f5f9);
    --hero-ink: #0f172a;
    --ghost-ink: rgba(15, 23, 42, .06);
    --ghost-ink-hover: rgba(15, 23, 42, .10);

    /* Gradient TEXT (background-clip: text) — BOTH stops are read as ink,
       so the pale second stop has to darken too. */
    --highlight-green: linear-gradient(90deg, #14532d, #166534);
    --highlight-blue: linear-gradient(90deg, #1e40af, #1d4ed8);
    --highlight-purple: linear-gradient(90deg, #5b21b6, #6d28d9);
    --highlight-orange: linear-gradient(90deg, #78350f, #9a3412);

    /* -- surfaces that ship as hard literals ------------------------------
       Each consumer is `var(--token, <the exact literal that was there>)`,
       so dark mode is byte-identical BY CONSTRUCTION rather than by my
       having retyped a value correctly. */
    --card-surface: #ffffff;
    --section-alt-surface: #e8eef6;
    --hero-chip-surface: linear-gradient(180deg, rgba(29, 78, 216, .10), rgba(29, 78, 216, .04)), #ffffff;
    --hero-price-surface: linear-gradient(180deg, rgba(29, 78, 216, .08), rgba(109, 40, 217, .04) 60%), #ffffff;
    --media-surface: #e2e8f0;
    --track-surface: rgba(15, 23, 42, .10);
    --track-inset: rgba(15, 23, 42, .18);
    --cta-scrim: linear-gradient(180deg, rgba(241, 245, 249, .82) 0%, rgba(241, 245, 249, .94) 100%);
    --rule-veil: rgba(15, 23, 42, .14);
    --chip-veil: rgba(15, 23, 42, .05);
    --chip-veil-strong: rgba(15, 23, 42, .07);
    --panel-veil: rgba(15, 23, 42, .04);
  }
}

/* Shared card surface: use this for any KAIS card-style container.
   Consumers may override --card-radius-lg via a more specific rule. */
.kais-surface,
.step-card,
.school-os-card,
.subject-card,
.showcase-tile,
.pricing-card-home,
.faq-item,
.hero-float-card {
  /* Two-stop glass wash + a 1px inner top light. The top light is what
     separates "a card" from "a lighter rectangle" on a dark page. */
  /* --card-surface is declared in the LIGHT blocks only (section 1b), so
     an undefined property here falls back to the two-stop glass wash
     below and dark mode is byte-identical by construction. */
  background: var(--card-surface,
    linear-gradient(180deg, rgba(255, 255, 255, .07), rgba(255, 255, 255, .015) 55%),
    rgba(18, 25, 57, .55));
  border: 1px solid var(--border-card);
  box-shadow:
    var(--shadow-card),
    var(--inner-top-light);
  -webkit-backdrop-filter: saturate(140%) blur(8px);
  backdrop-filter: saturate(140%) blur(8px);
  border-radius: var(--card-radius-lg);
}

/* Browsers without backdrop-filter get an opaque surface instead of a
   washed-out translucent one. */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .kais-surface,
  .step-card,
  .school-os-card,
  .subject-card,
  .showcase-tile,
  .pricing-card-home,
  .faq-item,
  .hero-float-card {
    background: var(--bg-card);
  }
}

/* 2. Scoped Homepage Overrides
   ------------------------------------------------------------ */
html {
  scroll-behavior: smooth;
}

.kais-home {
  /* Page-wide ambience: two very large, very soft accent washes that
     run BEHIND every section, so the page reads as one continuously
     lit space instead of a stack of flat slabs. Declared as background
     layers on <main> itself (not a pseudo-element) so there is no new
     stacking context to fight the subject-card tooltips at z-index 10. */
  background:
    radial-gradient(1100px 700px at 92% 8%, rgba(155, 109, 255, .10), transparent 70%),
    radial-gradient(900px 620px at -5% 42%, rgba(106, 168, 255, .09), transparent 70%),
    var(--bg-page) !important;
  color: var(--text-body) !important;
  font-family: var(--font);
  /* Nothing on this page may cause a sideways scroll — decorative
     glows and oversized orbs are deliberately allowed to sit outside
     the layout box. */
  overflow-x: clip;
}

.kais-home *,
.kais-home *::before,
.kais-home *::after {
  box-sizing: border-box;
}

/* Accessible skip link */
.skip-link {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
  z-index: 9999;
  background: var(--bg-card);
  color: var(--text-heading);
  padding: 0.75rem 1rem;
  border-radius: var(--radius-pill);
  border: 2px solid var(--brand);
}

.skip-link:focus {
  position: fixed;
  left: 50%;
  top: 1.25rem;
  transform: translateX(-50%);
  width: auto;
  height: auto;
}

/* 3. Layout Helpers
   ------------------------------------------------------------ */
.kais-container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: clamp(1rem, 4vw, 2rem);
}

/* Large displays: the 1140px cap left 759px of empty margin on a 1899px
   screen (MEASURED), squeezing the hero text column to 434px — which is
   what pushed the h1 onto two lines and the subtitle onto four, and made
   the price card look like a footnote on a big monitor. Widen the measure
   in one step above the common 1536px laptop width so 13"/14"/1440p
   layouts are untouched. */
@media (min-width: 1600px) {
  :root {
    --container: 1320px;
  }
}

.kais-section {
  padding-block: clamp(4rem, 8vw, 7rem);
  position: relative;
}

.kais-section-alt {
  /* Semi-transparent so the page-wide ambient wash on .kais-home shows
     through — an opaque slab would cut the lighting into stripes. */
  background: var(--section-alt-surface, rgba(14, 20, 41, .72));
}

/* Hairline seams between sections. A 1px gradient line that fades out
   at both ends reads as a considered edge rather than a hard cut. */
.kais-section-alt::before,
.kais-section-alt::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  height: 1px;
  background: linear-gradient(90deg,
              transparent,
              var(--rule-veil, rgba(255, 255, 255, .10)) 25%,
              var(--rule-veil, rgba(255, 255, 255, .10)) 75%,
              transparent);
  pointer-events: none;
}

.kais-section-alt::before { top: 0; }
.kais-section-alt::after { bottom: 0; }

.kais-section-title {
  font-size: clamp(1.75rem, 3.5vw, 2.5rem);
  font-weight: 800;
  color: var(--text-heading);
  line-height: 1.15;
  /* Large display type set at default tracking looks loose; tightening
     it is most of what separates "big bold text" from "a headline". */
  letter-spacing: -0.025em;
  text-wrap: balance;
  margin: 0;
}

.kais-section-subtitle {
  font-size: clamp(1rem, 2vw, 1.15rem);
  color: var(--text-body);
  line-height: 1.6;
  margin: 0;
  max-width: 36rem;
  /* CSP-N05 follow-up: keep subtitle text centered without an
     inline ``style="text-align:center;"`` attribute. */
  text-align: center;
  text-wrap: pretty;
}

/* Shared section-header rhythm
   ------------------------------------------------------------
   Every section opened with a bare h2 + p, which gave the page no
   sense of arrival — each block just started. The eyebrow labels
   the section, the rule under the title provides a consistent
   optical anchor, and the fixed gap ladder makes the vertical
   rhythm identical from section to section. */
.section-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.85rem;
}

.section-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.85rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--stroke-soft);
  background: var(--chip-veil, rgba(255, 255, 255, .04));
  box-shadow: var(--inner-top-light);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-body);
}

/* Small accent dot so the eyebrow reads as a label, not a button. */
.section-eyebrow::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--gradient-cta);
  flex: 0 0 auto;
}

/* Decorative gradient underline. Deliberately short and centred —
   it anchors the title without competing with it. */
.section-rule {
  display: block;
  width: 64px;
  height: 3px;
  border-radius: var(--radius-pill);
  background: var(--gradient-cta);
  opacity: 0.9;
}

.kais-section-alt .section-eyebrow {
  background: var(--chip-veil-strong, rgba(255, 255, 255, .05));
}

.text-center {
  text-align: center;
}

.mx-auto {
  margin-inline: auto;
}

/* Gradient-highlighted accent words */
.highlight-green {
  background: var(--highlight-green, linear-gradient(90deg, var(--ok), #5dffb0));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.highlight-blue {
  background: var(--highlight-blue, linear-gradient(90deg, var(--brand), #92c5ff));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.highlight-purple {
  /* Starts brighter than --brand-2: measured against the dark showcase
     background the raw token read noticeably dimmer than its blue and
     green siblings, so the three accent words looked inconsistent. */
  background: var(--highlight-purple, linear-gradient(90deg, #b98cff, #dcc6ff));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.highlight-orange {
  background: var(--highlight-orange, linear-gradient(90deg, #FF9600, #FFB74D));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* 4. Buttons
   ------------------------------------------------------------ */
.btn-kais-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  background: var(--cta-surface, var(--gradient-cta));
  color: #fff;
  border: none;
  border-radius: var(--radius-btn);
  padding: 0.9rem 2rem;
  font-weight: 700;
  font-size: 1.05rem;
  font-family: var(--font);
  cursor: pointer;
  text-decoration: none;
  transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease;
  box-shadow: var(--shadow-btn);
  position: relative;
  overflow: hidden;
  z-index: 1;
}

.btn-kais-primary::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, var(--brand-2), var(--brand));
  opacity: 0;
  transition: opacity 0.3s ease;
  z-index: -1;
}

.btn-kais-primary:hover {
  transform: translateY(-3px) scale(1.02);
  box-shadow: 0 14px 34px -8px rgba(106, 168, 255, .55), var(--inner-top-light);
}

.btn-kais-primary:hover::before {
  opacity: 1;
}

.btn-kais-primary:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
}

/* Specular sweep on hover.
   Scoped to `.hero-actions` on purpose: `animations.css` owns
   `.final-cta-section .btn-kais-primary::after` for the GSAP-driven
   shimmer, and two rules fighting over the same pseudo-element would
   merge into a half-applied hybrid. */
.hero-actions .btn-kais-primary::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--sheen);
  background-size: 250% 100%;
  background-position: 150% 0;
  opacity: 0;
  pointer-events: none;
  transition: background-position .75s var(--ease-out), opacity .25s ease;
}

.hero-actions .btn-kais-primary:hover::after,
.hero-actions .btn-kais-primary:focus-visible::after {
  background-position: -50% 0;
  opacity: 1;
}

.btn-kais-primary.btn-large {
  padding: 1rem 2.5rem;
  font-size: 1.15rem;
}

.btn-kais-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  background: transparent;
  color: var(--text-heading);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-btn);
  padding: 0.85rem 1.8rem;
  font-weight: 600;
  font-size: 1.05rem;
  font-family: var(--font);
  cursor: pointer;
  text-decoration: none;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

.btn-kais-ghost {
  box-shadow: var(--inner-top-light);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.btn-kais-ghost:hover {
  border-color: rgba(106, 168, 255, .55);
  background: rgba(106, 168, 255, .10);
  transform: translateY(-2px);
  box-shadow: 0 10px 26px -12px rgba(106, 168, 255, .5), var(--inner-top-light);
}

.btn-kais-ghost:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
}

/* 5. Hero Section
   ------------------------------------------------------------ */
/* Hero height budget (MEASURED at 390x844 / 1280x800 / 1440x900, not
   computed — see the fold check under the session scratchpad).
   The hero must fit badge + h1 + subtitle + price block + CTA above the
   fold on a 1280x800 laptop, so padding, gaps and type maxima here are
   a budget, not free-form styling. Re-measure before growing any of
   them. */
.hero-section {
  /* Four-layer lighting rig, top-lit and cooling toward the fold:
     a warm purple key from top-right, a blue fill from the left, a
     tight highlight directly behind the headline so the type sits in
     its own pool of light, and a floor shadow that hands off to the
     next section. */
  background:
    radial-gradient(900px 520px at 78% -12%, rgba(155, 109, 255, .30), transparent 72%),
    radial-gradient(760px 460px at -8% 6%, rgba(106, 168, 255, .26), transparent 72%),
    radial-gradient(520px 300px at 26% 24%, rgba(122, 150, 255, .14), transparent 70%),
    linear-gradient(180deg, transparent 55%, rgba(6, 9, 20, .55)),
    var(--bg-page);
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding-top: clamp(1.5rem, 3vw, 2.5rem);
  /* MEASURED at 1899px: hero bottom padding (80px) + wave divider (79px)
     + the next section's top padding (112px) stacked into a 261px empty
     band between the hero and "Explore N+ Subjects" — the page read as
     half-empty on a large monitor. The wave divider is already doing the
     separating work, so the hero does not need to pad against it too. */
  padding-bottom: clamp(2rem, 3.5vw, 3rem);
}

/* The section immediately after the wave divider inherits the generic
   7rem section padding, which double-counts the divider's own 79px. Pull
   just that one boundary in; every other section keeps the site rhythm. */
.wave-divider + .kais-section {
  padding-top: clamp(2rem, 4vw, 3.5rem);
}

/* Decorative orb shapes. Now larger, softer and slowly drifting so the
   hero breathes instead of sitting perfectly still. */
.hero-section::before,
.hero-section::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  filter: blur(30px);
  opacity: 0.4;
  will-change: transform;
}

.hero-section::before {
  width: 340px;
  height: 340px;
  background: radial-gradient(circle, rgba(106, 168, 255, .38), transparent 70%);
  top: -120px;
  right: -110px;
  animation: orb-drift-a 19s ease-in-out infinite;
}

.hero-section::after {
  width: 280px;
  height: 280px;
  background: radial-gradient(circle, rgba(155, 109, 255, .34), transparent 70%);
  bottom: -90px;
  left: -70px;
  animation: orb-drift-b 23s ease-in-out infinite;
}

/* Transform-only drift: composited, so it costs no layout or paint. */
@keyframes orb-drift-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(-26px, 22px, 0) scale(1.08); }
}

@keyframes orb-drift-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(24px, -20px, 0) scale(1.1); }
}

/* Hero FX layer
   ------------------------------------------------------------
   Both hero pseudo-elements are already spent on the orbs, so the
   texture + aurora need their own element. Purely decorative:
   aria-hidden in the markup, `pointer-events: none` here (it spans
   the whole hero, including the CTA buttons — without it the primary
   call to action would silently stop being clickable). */
.hero-fx {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  /* Fine dot texture, faded out toward the bottom so it never
     competes with the copy sitting on top of it. */
  background-image: var(--dot-grid);
  background-size: var(--dot-grid-size);
  -webkit-mask-image: radial-gradient(120% 85% at 50% 0%, #000 25%, transparent 78%);
  mask-image: radial-gradient(120% 85% at 50% 0%, #000 25%, transparent 78%);
}

/* Slow aurora band sweeping across the top of the hero. */
.hero-fx::before {
  content: "";
  position: absolute;
  inset-inline: -30%;
  top: -40%;
  height: 90%;
  pointer-events: none;
  background: conic-gradient(from 210deg at 50% 50%,
              rgba(106, 168, 255, 0) 0deg,
              rgba(106, 168, 255, .18) 70deg,
              rgba(155, 109, 255, .20) 150deg,
              rgba(255, 150, 0, .07) 210deg,
              rgba(106, 168, 255, 0) 320deg);
  filter: blur(60px);
  opacity: .55;
  animation: aurora-turn 34s linear infinite;
}

/* Horizon seam: a bright hairline where the hero meets the fold. */
.hero-fx::after {
  content: "";
  position: absolute;
  inset-inline: 10%;
  bottom: 0;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(90deg,
              transparent,
              rgba(155, 109, 255, .45),
              rgba(106, 168, 255, .45),
              transparent);
  opacity: .7;
}

@keyframes aurora-turn {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

.hero-grid {
  display: grid;
  grid-template-columns: 1fr;
  /* Row and column gaps are declared SEPARATELY, and the breakpoints
     below tune only the COLUMN gap. The grid now carries a full-width
     `.hero-head` row, and the column gap that separates the copy from
     the photo (3.5-4rem) is far too much air under a headline — the
     badge/h1/subtitle used to be one flex stack with a 1rem gap. A
     `gap:` shorthand in either breakpoint would silently reset the row
     gap and put that 4rem back. */
  column-gap: 3rem;
  row-gap: 2rem;
  align-items: center;
  position: relative;
  z-index: 1;
}

/* The headline row spans every column — this is what puts each hero
   line on ONE row instead of two. MEASURED: 434-511px inside
   `.hero-text` vs ~1076px here, against a 669px requirement. */
.hero-head {
  grid-column: 1 / -1;
}

/* The badge carries a `text-center` utility in the template. Inside the
   old narrow text column that centred it more or less over the
   headline; across the full-width head row it centres over ~1076px and
   floats away from the left-aligned h1. Anchor it to the same edge as
   the headline it introduces. */
.hero-head #site-hero-badge {
  text-align: left;
}

@media (min-width: 900px) {
  .hero-grid {
    grid-template-columns: 1fr 1fr;
    column-gap: 4rem;
  }
}

@media (min-width: 1024px) {
  .hero-grid {
    /* Was 1fr / 2fr — a ~356px text column at 1280px, which forced the
       h1 onto three lines and the subtitle onto three. Rebalanced so the
       copy that now carries the price gets room to breathe; the photo is
       still the larger half. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
    column-gap: 3.5rem;
  }
}

.hero-text {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.hero-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  /* Tinted glass + a top-light seam so the badge reads as a raised
     chip rather than a flat colour swatch.

     The rgba(11,16,32,.6) BASE is a contrast fix, not styling. Without it the
     tint is fully translucent, so the chip's background is whatever the hero
     happens to paint behind it — and the hero's blue key light is strongest
     exactly where the badge sits. MEASURED, the #a9cdff label came out at
     4.48:1 (320px) and 4.56:1 (390px) against a 4.5:1 requirement, i.e. at or
     under the line on the two smallest phones. With its own base the chip
     carries its own contrast: 6.8:1 at 320px, 6.7:1 at 390px, 7.7:1 at
     1440px, and it no longer moves when the lighting behind it does. */
  background: var(--hero-chip-surface,
    linear-gradient(180deg, rgba(106, 168, 255, .20), rgba(106, 168, 255, .09)),
    rgba(11, 16, 32, .6));
  border: 1px solid rgba(106, 168, 255, .30);
  box-shadow: var(--inner-top-light), 0 6px 20px -12px rgba(106, 168, 255, .8);
  color: var(--accent-ink, #a9cdff);
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius-pill);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  line-height: 1.25;
  text-transform: uppercase;
  width: fit-content;
}

.hero-badge .fas {
  color: var(--accent-ink, var(--brand));
  /* Tiny glow on the cap icon — the one spot of "spark" at the very
     top of the page. */
  filter: drop-shadow(0 0 6px rgba(106, 168, 255, .8));
}

/* ──────────────────────────────────────────────
   Hero Badge — Cinematic animated pill
   ────────────────────────────────────────────── */

.hero-badge-animated {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 1.2rem;
  border-radius: var(--radius-pill);
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-ink, #a9cdff);
  width: fit-content;

  /* Tinted glass base */
  background: var(--hero-chip-surface,
    linear-gradient(180deg, rgba(106, 168, 255, .20), rgba(106, 168, 255, .09)),
    rgba(11, 16, 32, .65));
  border: 1px solid rgba(106, 168, 255, .35);

  /* Pulsing glow shadow */
  box-shadow:
    var(--inner-top-light),
    0 0 20px -4px rgba(106, 168, 255, .4),
    0 0 60px -12px rgba(106, 168, 255, .2);

  /* Entrance: slide up + fade in */
  opacity: 0;
  transform: translateY(12px) scale(0.97);
  animation: hero-badge-enter 0.8s cubic-bezier(.22,1,.36,1) 0.2s forwards,
             hero-badge-glow 4s ease-in-out 2s infinite;

  /* Shimmer overlay that sweeps across the badge border */
  position: relative;
  overflow: hidden;
}

.hero-badge-animated::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(
    105deg,
    transparent 30%,
    rgba(106, 168, 255, .25) 48%,
    rgba(106, 168, 255, .35) 50%,
    rgba(106, 168, 255, .25) 52%,
    transparent 70%
  );
  background-size: 250% 100%;
  background-position: 150% center;
  animation: badge-shimmer 5s ease-in-out 3s infinite;
  pointer-events: none;
}

/* Staggered letter reveal for the badge text */
.hero-badge-animated {
  animation: hero-badge-enter 0.8s cubic-bezier(.22,1,.36,1) 0.2s forwards,
             hero-badge-glow 4s ease-in-out 2s infinite;
}

@keyframes hero-badge-enter {
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes hero-badge-glow {
  0%, 100% {
    box-shadow:
      var(--inner-top-light),
      0 0 20px -4px rgba(106, 168, 255, .4),
      0 0 60px -12px rgba(106, 168, 255, .2);
    border-color: rgba(106, 168, 255, .35);
  }
  50% {
    box-shadow:
      var(--inner-top-light),
      0 0 28px -2px rgba(106, 168, 255, .6),
      0 0 90px -8px rgba(106, 168, 255, .35);
    border-color: rgba(106, 168, 255, .6);
  }
}

@keyframes badge-shimmer {
  0%   { background-position: 150% center; }
  40%, 100% { background-position: -50% center; }
}


/* ──────────────────────────────────────────────
   Hero Heading — Two-line staggered reveal
   ────────────────────────────────────────────── */

.hero-heading {
  /* Max capped at 3rem: the hero must leave room for the price block
     above a 1280x800 fold. MEASURED — 3.5rem put the heading on three
     rows and pushed the price to bottom=825 against an 800px fold.
     NOTE this cap does NOT control how many ROWS each line wraps to:
     at 1280-1920 the first line needs ~713px on one row against a
     434-511px text column, and 3rem only brings that to ~668px. The
     wrap is a COLUMN-WIDTH property, not a font-size one. Pinned by
     test_home_hero_pricing.py::
     test_hero_heading_max_size_stays_within_the_height_budget. */
  font-size: clamp(2rem, 4vw, 3rem);
  font-weight: 800;
  color: var(--text-heading);
  line-height: 1.12;
  letter-spacing: -0.03em;
  text-wrap: balance;
  margin: 0;
}

/* Each hero line enters independently */
.hero-line {
  display: block;
  opacity: 0;
  transform: translateY(18px);
}

/* NO `background-clip: text` ON THIS HEADING. It is the one construct
   that provably breaks here, and this is the LCP element.
   `home.js:splitWords()` re-wraps every word of the <h1> in a
   `display: inline-block` span for the GSAP cascade, and GSAP leaves a
   real `transform: translate(0px, 0px)` + `will-change` on each one.
   Blink mispaints a text clip in that situation — BOTH placements were
   tried against the reporter's Chrome and BOTH failed: on the LINE the
   clip cannot reach its atomic inline-block words (headless Chromium
   painted the whole heading blank; their Chrome collapsed each line's
   wrapped rows into overlapping smears), and repainted on each
   `.gsap-word` the clip sits on the transformed element itself and
   smeared again. Firefox renders both correctly, so neither failure
   shows up locally and neither was reproducible here.
   Solid colours have ONE code path, cannot mis-clip in any engine, and
   at heading size read near-identically to the gradient. The shimmer
   went with it — it animated `background-position`, which needs the
   clip. If gradient text is ever wanted back, remove the word-splitting
   from this heading first; the two cannot coexist.
   Pinned by test_home_gsap_invisible_regression.py::
   test_nothing_in_the_split_hero_heading_paints_via_a_text_clip. */
.hero-line--primary {
  color: var(--hero-ink, #ffffff);
  animation: hero-line-enter 0.9s cubic-bezier(.22,1,.36,1) 0.5s forwards;
}

.hero-line--secondary {
  font-style: italic;
  color: var(--accent-ink, #7db4ff);
  animation: hero-line-enter 0.9s cubic-bezier(.22,1,.36,1) 0.8s forwards;

  /* Soft blue glow behind the italic line. A `filter` is safe here —
     unlike a text clip it does not depend on where the glyphs live. */
  filter: drop-shadow(0 0 18px rgba(106, 168, 255, .45))
          drop-shadow(0 2px 8px rgba(106, 168, 255, .3));
}

/* THE HERO LINES ARE SPLIT WORD-BY-WORD AT RUNTIME — and a gradient
   clip does not survive that on its own.
   `home.js:splitWords()` re-wraps every word of the `<h1>` in a
   `display: inline-block` span so GSAP can cascade them in. An atomic
   inline-block descendant is NOT part of its parent's text clip, so the
   rules above are left clipping nothing: MEASURED in headless Chromium
   the heading painted zero glyphs, and in the reporter's Chrome the two
   wrapped rows of each line collapsed into overlapping smears. Firefox
   paints it correctly, so this ships looking fine locally.
   `animations.css` documents (and fixes) the identical hazard for
   `.highlight-orange`; the hero lines gained their gradient later and
   never got the companion rule.
   The parent rules stay exactly as they are — they are what paints when
   nothing splits the heading (reduced motion, GSAP blocked, JS off).
   Pinned by test_home_gsap_invisible_regression.py. */
/* ...and the parent must STOP painting, or the heading renders twice.
   Chrome does pull descendant text into the parent's clip mask — it
   just computes the offset wrong for an atomic inline-block, collapsing
   every wrapped row onto the first. With the companion rule below in
   place but this one absent, the hero showed FOUR rows: the correct
   per-word paint plus the parent's collapsed smear underneath.
   `background-image: none` leaves nothing to clip and nothing to paint,
   in any engine. Scoped with `:has()` so the parent keeps its own
   gradient whenever nothing splits it (reduced motion, GSAP blocked, JS
   off) — that is the fallback the rules above exist for.
   SPECIFICITY: `:has()` takes the specificity of its most specific
   argument, so `.hero-line:has(.gsap-word)` is (0,2,0) and beats
   `.hero-line--primary` (0,1,0) regardless of source order.
   Pre-`:has()` engines (Firefox < 121) keep both paints, but Firefox
   positions the parent paint CORRECTLY, so the two coincide and the
   degradation is invisible. */
.hero-line:has(.gsap-word) {
  /* GSAP owns the entrance now — stop the CSS keyframe from fighting
     it. Without opacity: 1 the line starts at base opacity: 0 and stays
     invisible until hero-line-enter fires 0.5s later, which compounds
     with every child's transform and produces overlapping smears on
     Chromium. transform is equally important: base state has
     translateY(18px) that never reaches the keyframe to-state when
     animation is suppressed. Pinned by test_home_hero_line_stops_css_when_gsap_ready.py.*/
  opacity: 1;
  transform: none;
  background-image: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  animation: none;
}

/* The split words inherit their line's `color` and paint themselves.
   No background, no clip, nothing that cares about being transformed —
   which is the entire point. Kept as an explicit rule so the intent
   survives: this is where a text clip would be re-introduced, and it
   must not be. */
.hero-line .gsap-word {
  color: inherit;
}

@keyframes hero-line-enter {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes hero-text-shimmer {
  0%   { background-position: 0% center; }
  100% { background-position: 200% center; }
}

/* The price word (".highlight-orange") is the single most important glyph run on the
   page — give it its own light source. `drop-shadow` (not text-shadow)
   because the word is painted with background-clip: text, where
   text-shadow would draw behind the transparent fill and show as a
   grey smear. */
.hero-heading .highlight-orange {
  filter: drop-shadow(0 4px 18px rgba(255, 150, 0, .38));
}

.hero-subtitle {
  font-size: clamp(1rem, 1.6vw, 1.1rem);
  color: var(--text-body);
  line-height: 1.6;
  margin: 0;
  max-width: 34rem;
  text-wrap: pretty;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

/* Above-the-fold price block
   ------------------------------------------------------------
   Answers "what does it cost?" without a scroll. Sits between the
   subtitle and the CTA row so the number lands before the buttons
   on a phone, where the hero stacks vertically.

   Height budget matters here — every pixel this block adds pushes
   the CTA row toward the fold. Kept to one tile row + one claim
   line, with the tile grid PINNED to 3 columns (never wrap) and the
   type scaled down below 640px. */
.hero-price {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin-top: 0.25rem;
  padding: 0.9rem 1rem;
  border: 1px solid rgba(106, 168, 255, .30);
  border-radius: var(--card-radius);
  /* Lit glass: a cool wash over a dark base, a 1px top light, and a
     coloured drop shadow. This is the "premium" surface on the page —
     everything else is deliberately quieter than it. */
  background: var(--hero-price-surface,
    linear-gradient(180deg, rgba(106, 168, 255, .16), rgba(155, 109, 255, .07) 60%),
    rgba(11, 16, 32, .5));
  box-shadow:
    var(--inner-top-light),
    0 18px 44px -24px rgba(106, 168, 255, .85);
  -webkit-backdrop-filter: blur(10px) saturate(130%);
  backdrop-filter: blur(10px) saturate(130%);
  max-width: 32rem;
}

/* One slow specular pass across the card, on a long loop. Enough to
   catch the eye on arrival; not enough to be a distraction while
   someone is reading the price. `pointer-events: none` is mandatory —
   this layer covers the "See the full comparison" link. */
.hero-price::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: var(--sheen);
  background-size: 220% 100%;
  /* MANDATORY, and the whole reason this comment is long.
     `background-repeat` defaults to `repeat`. At `background-size: 220%` the
     sheen therefore TILES every 220% of the card width, so the "parked"
     position below never actually removed the highlight — the neighbouring
     tile wrapped a specular band straight back over the RIGHT THIRD of the
     card. MEASURED, `.hero-price-caption` on the third tile:
       repeat (as shipped)  3.61:1 p2 / 4.25:1 median  @1440x900
                            3.56:1 p2 / 4.20:1 median  @390x844
       no-repeat            5.38:1 p2 / 5.54:1 median  @1440x900
     against a 4.5:1 requirement. And it was PERMANENT for reduced-motion
     users, because the reduce block sets `animation: none` and pins the
     element at exactly this rest position. */
  background-repeat: no-repeat;
  /* Base position parks the highlight OFF the card — genuinely off, now that
     the tile cannot wrap. Load-bearing for reduced motion: with
     `animation: none` the element falls back to this declared position, and
     the default `0% 0` would leave a permanent white smear across the price.
     With no-repeat, 190% puts the image's right edge at -8% of the card, and
     the animated sweep to -60% still carries the band across exactly once. */
  background-position: 190% 0;
  /* Dimmed. At full strength the band is rgba(255,255,255,.16) sweeping the
     FULL WIDTH of the card, and MEASURED across its whole animated range it
     dragged every caption to 2.8-3.7:1 once every 9 seconds, at every
     viewport — text that meets 1.4.3 only part of the time does not meet it.
     Masking the band to the top of the card was tried and rejected: at 320px
     the card is compact enough that any mask soft enough to still read as a
     glint still reached the caption row (best variant left cap1 at 3.80:1).
     Dimming plus the caption's own colour bump clears 4.5:1 at every position
     and viewport while keeping the pass visible. */
  opacity: .6;
  animation: price-sheen 9s ease-in-out infinite;
}

@keyframes price-sheen {
  0%, 62%, 100% { background-position: 190% 0; }
  86%           { background-position: -60% 0; }
}

.hero-price-tiles {
  /* Explicit 3-column grid, NOT flex-wrap: three tiles dropping to a
     second row on a narrow phone would push the CTA row below the
     fold — the exact regression this block exists to prevent. */
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
}

.hero-price-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  padding-inline: 0.6rem;
  /* Logical property so the divider flips correctly under RTL. */
  border-inline-start: 1px solid var(--border-light);
}

.hero-price-tile:first-child {
  padding-inline-start: 0;
  border-inline-start: 0;
}

/* The lead tile ($10/month) is the offer — everything else on the card
   is supporting evidence. A soft accent wash behind it does the
   ranking without adding a badge (which would cost vertical budget the
   above-the-fold contract cannot spare). */
.hero-price-tile--lead {
  border-radius: 12px;
  margin-inline-start: -0.35rem;
  padding-inline-start: 0.35rem;
  background: linear-gradient(180deg, rgba(106, 168, 255, .14), transparent 85%);
}

.hero-price-amount {
  font-size: clamp(1.35rem, 3.4vw, 1.85rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--text-heading);
}

.hero-price-tile--lead .hero-price-amount {
  background: var(--gradient-cta-ink, var(--gradient-cta));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 3px 14px rgba(106, 168, 255, .45));
}

.hero-price-unit {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-body);
}

.hero-price-caption {
  font-size: 0.7rem;
  line-height: 1.35;
  /* --text-body, not --text-muted-strong. This is the smallest copy on the
     page (0.7rem, shrinking to 0.6rem at <=359px) AND it sits on the one
     surface with a specular sweep passing over it, so the muted token had no
     headroom left: MEASURED across the sheen's full animated range it bottomed
     out at 2.81:1 (320px), 3.03:1 (390px), 4.02:1 (1440px) against a 4.5:1
     requirement. Paired with the sheen dim on `.hero-price::after`, the same
     pixels now measure 5.7:1+ at every viewport and every sheen position.
     Hierarchy is carried by size and weight (0.7rem/400 against the unit's
     0.78rem/600), which is enough at this scale — colour was not. */
  color: var(--text-body);
}

.hero-price-claim {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem;
  margin: 0.6rem 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border-light);
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--text-body);
}

.hero-price-claim strong {
  color: var(--text-heading);
  font-weight: 700;
}

.hero-price-claim .fa-circle-check {
  color: var(--ok-ink, var(--ok));
  filter: drop-shadow(0 0 6px rgba(47, 211, 122, .55));
}

.hero-price-link {
  position: relative;
  color: var(--accent-ink, var(--brand));
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}

/* Target size (WCAG 2.2 SC 2.5.8, 24x24 minimum).
   MEASURED 181.4 x 18.1 CSS px at 390x844 — 6px short.
   The hero's above-the-fold budget forbids growing the element: padding here
   pushes `.hero-actions` toward the fold, which is the exact regression the
   price block exists to prevent. So the POINTER TARGET is expanded by an
   absolutely-positioned pseudo-element and the LAYOUT does not move by a
   pixel. `.hero-price::after` sits above this at z-index 2 but carries
   `pointer-events: none`, so it cannot steal the tap. */
.hero-price-link::before {
  content: "";
  position: absolute;
  inset: -5px -4px;
  border-radius: 6px;
}

.hero-price-link:hover,
.hero-price-link:focus-visible {
  text-decoration: underline;
}

.hero-price-link:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Social proof strip */
.hero-proof {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin-top: 0.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border-light);
}

.proof-item {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
}

.proof-number {
  font-size: 1.4rem;
  font-weight: 800;
  background: var(--gradient-cta-ink, var(--gradient-cta));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.proof-label {
  font-size: 0.85rem;
  color: var(--text-body);
  font-weight: 500;
}

/* Hero visual (decorative right side) */
.hero-visual {
  display: block;
  position: relative;
  /* On mobile let the photo's intrinsic 16:9 aspect drive height; cap width
     so the photo doesn't stretch full-bleed on phones. */
  max-width: 540px;
  margin-left: auto;
  margin-right: auto;
}

@media (min-width: 900px) {
  .hero-visual {
    max-width: none;
    margin-left: 0;
    margin-right: 0;
    min-height: 320px;
  }
}

/* Floating subject cards decoration */
.hero-card-stack {
  position: relative;
  width: 100%;
  height: 100%;
}

@media (min-width: 900px) {
  .hero-card-stack {
    min-height: 320px;
  }
}

.hero-float-card {
  position: absolute;
  border-radius: var(--card-radius);
  padding: 0.85rem 1.1rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--text-heading);
  text-decoration: none;
  /* Heavier glass than the shared surface: these float over a bright
     photograph and need to hold their own edge.

     Contrast checked and left ALONE. The shared `.kais-surface` wash is only
     rgba(18,25,57,.55) over the hero photo, which looks like it should strand
     the label — but MEASURED over the actual glyph runs, all four cards sit
     between 8.6:1 and 12.6:1 at every viewport where they render (660 /
     768 / 900 / 1024 / 1280 / 2560; they are display:none below 640). The
     bright pixels inside these cards belong to `.hero-float-icon`, which is
     painted the per-subject colour and carries no text. */
  border-color: var(--stroke-strong);
  box-shadow:
    0 18px 40px -20px rgba(0, 0, 0, .9),
    var(--inner-top-light);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  backdrop-filter: saturate(160%) blur(14px);
  transition: transform .35s var(--ease-spring), box-shadow .3s ease,
              border-color .3s ease;
  animation: float-card 6s ease-in-out infinite;
}

.hero-float-card:hover,
.hero-float-card:focus-visible {
  border-color: rgba(106, 168, 255, .5);
  box-shadow:
    0 22px 50px -18px rgba(0, 0, 0, .9),
    var(--glow-blue),
    var(--inner-top-light);
}

.hero-float-card:nth-child(1) {
  top: 10%;
  left: 10%;
  animation-delay: 0s;
}

.hero-float-card:nth-child(2) {
  top: 35%;
  right: 5%;
  animation-delay: -1.5s;
}

.hero-float-card:nth-child(3) {
  bottom: 20%;
  left: 15%;
  animation-delay: -3s;
}

.hero-float-card:nth-child(4) {
  bottom: 5%;
  right: 10%;
  animation-delay: -4.5s;
}

.hero-float-icon {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.35rem;
  line-height: 1;
  flex-shrink: 0;
  /* Gloss + rim light on the coloured tile. The per-subject colour is
     set as `background` by the nonced <style> block, so this rule may
     only add box-shadow — a `background` here would overwrite it. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .45),
    inset 0 -6px 12px -6px rgba(0, 0, 0, .5),
    0 6px 14px -6px rgba(0, 0, 0, .8);
}

@keyframes float-card {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

/* Hero avatar video */
.hero-avatar {
  margin-top: 1.5rem;
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  border: 1px solid var(--border-light);
}

.hero-avatar-video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-card);
}

/* 6. Wave Divider
   ------------------------------------------------------------ */
.wave-divider {
  line-height: 0;
  overflow: hidden;
  position: relative;
  /* Bleed slightly into the hero so the wave crest overlaps the hero's
     horizon seam instead of butting against it. */
  margin-top: -1px;
}

.wave-divider svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Two stacked crests: a faint back wave offset from the front one
   gives the divider depth, so the section transition reads as a
   layered horizon rather than a single cut-out. */
.wave-divider path {
  fill: var(--section-alt-surface, rgba(14, 20, 41, .72));
}

.wave-divider .wave-back {
  fill: rgba(155, 109, 255, .10);
}

/* 7. Subjects Showcase
   ------------------------------------------------------------ */
.subjects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 1rem;
  margin-top: clamp(2.5rem, 4vw, 3.25rem);
}

.subject-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  padding: 1.25rem 0.75rem;
  border-radius: var(--card-radius);
  border-color: color-mix(in srgb, var(--card-color, var(--brand)) 30%, var(--border-card));
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease, border-color 0.2s ease;
  position: relative;
  /* `visible` (not `hidden`) so the hover/focus .subject-card-info tooltip
     can pop OUTSIDE the card bounds without being clipped. */
  overflow: visible;
}

.subject-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--brand), var(--brand-2));
  /* Round the top corners to match the card now that overflow is visible
     (the card no longer clips this accent bar to its rounded shape). */
  border-top-left-radius: var(--card-radius);
  border-top-right-radius: var(--card-radius);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s ease;
  z-index: 2;
}

.subject-card:hover::before {
  transform: scaleX(1);
}

/* Per-subject aura. Uses the card's own --card-color (set by the
   nonced <style> block) so every card glows in its own hue on hover.
   `pointer-events: none` is REQUIRED: the card IS the <a>, and a
   full-bleed overlay without it makes the whole subject grid look
   clickable while silently swallowing every click. */
.subject-card::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(120% 80% at 50% 0%,
              color-mix(in srgb, var(--card-color, var(--brand)) 26%, transparent),
              transparent 70%);
  opacity: 0;
  transition: opacity .35s ease;
}

.subject-card:hover::after,
.subject-card:focus-visible::after,
.subject-card:focus-within::after {
  opacity: 1;
}

/* Keep the card's real content above the aura layer.
   Named children only — a blanket `.subject-card > *` would apply
   `position: relative` to `.subject-card-info` (absolutely positioned
   tooltip) and `.subject-free-badge` (absolutely positioned corner
   badge), breaking both, and would only "work" by relying on later
   source order to win an equal-specificity fight. */
.subject-card > .subject-icon,
.subject-card > .subject-card-name {
  position: relative;
  z-index: 1;
}

.subject-card:hover {
  transform: translateY(-6px) scale(1.02);
  box-shadow:
    var(--shadow-card-hover),
    0 20px 46px -26px color-mix(in srgb, var(--card-color, var(--brand)) 85%, transparent),
    var(--inner-top-light);
  border-color: var(--card-color, var(--border-card));
}

.subject-card .subject-icon {
  width: 48px;
  height: 48px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.75rem;
  line-height: 1;
  background: var(--card-color, var(--brand));
  /* Gloss + rim, matching the hero float-card icons so the two
     surfaces read as the same component family. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .5),
    inset 0 -8px 14px -8px rgba(0, 0, 0, .55),
    0 8px 18px -8px color-mix(in srgb, var(--card-color, var(--brand)) 80%, transparent);
  transition: transform 0.4s var(--ease-spring), box-shadow 0.3s ease;
}

.subject-card:hover .subject-icon {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .6),
    inset 0 -8px 14px -8px rgba(0, 0, 0, .55),
    0 12px 26px -8px color-mix(in srgb, var(--card-color, var(--brand)) 95%, transparent);
}

.subject-card .subject-icon .subject-icon-logo,
.hero-float-icon .subject-icon-logo {
  width: 75%;
  height: 75%;
  object-fit: contain;
  display: block;
}

.subject-card:hover .subject-icon {
  transform: scale(1.15) rotate(-5deg);
}

.subject-card > .subject-card-name {
  font-weight: 600;
  font-size: 0.88rem;
  color: var(--text-heading);
  text-align: center;
  line-height: 1.3;
}

/* --- Subject-card hover/focus info tooltip -------------------------------- */

/* Raise the hovered / keyboard-focused card so its tooltip paints above
   neighbouring cards in the grid. */
.subject-card:hover,
.subject-card:focus-visible,
.subject-card:focus-within {
  z-index: 5;
}

/* Focus ring (WCAG 2.4.7 / 2.4.11).
   These are the most-used links on the page and they were the ONLY
   interactive elements here with no authored indicator — MEASURED, the
   computed style fell back to Chromium's UA ring (`outline: auto 1px
   rgb(16,16,16)`), whose dominant pixel came out at 1.08:1 against the card
   backdrop. --brand measures ~7:1 on --bg-alt.

   The offset is POSITIVE: the card is deliberately `overflow: visible` (so
   its tooltip can escape), so nothing clips the ring, and an inset ring would
   paint over the card's own top accent bar. */
.subject-card:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 3px;
}

.subject-card-info {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  z-index: 10;
  width: min(17rem, 80vw);
  box-sizing: border-box;
  padding: 0.85rem 0.95rem;
  border-radius: var(--card-radius);
  background: var(--bg-card);
  border: 1px solid var(--border-card);
  /* Accent edge tinted by the per-card colour (set via the nonced
     <style> block as --card-color). */
  border-top: 3px solid var(--card-color, var(--brand));
  box-shadow: var(--shadow-card-hover);
  text-align: left;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%) translateY(6px);
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275), visibility 0.25s ease;
}

/* Downward caret bridging the tooltip to the card. */
.subject-card-info::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-top: 8px solid var(--bg-card);
}

.subject-card-tagline {
  display: block;
  font-weight: 700;
  font-size: 0.86rem;
  line-height: 1.35;
  color: var(--card-color, var(--brand));
}

.subject-card-desc {
  display: block;
  margin-top: 0.4rem;
  font-size: 0.76rem;
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-body);
}

.subject-card-teachers {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.6rem;
  padding-top: 0.55rem;
  border-top: 1px solid var(--border-card);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--card-color, var(--brand));
}

.subject-card-teachers i {
  font-size: 0.8rem;
  flex: 0 0 auto;
}

/* Reveal on pointer hover AND keyboard focus (never hover-only). */
.subject-card:hover .subject-card-info,
.subject-card:focus-visible .subject-card-info,
.subject-card:focus-within .subject-card-info {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}

/* Reduced motion: appear instantly, no slide. */
@media (prefers-reduced-motion: reduce) {
  .subject-card-info {
    transition: none;
    transform: translateX(-50%);
  }

  .subject-card:hover .subject-card-info,
  .subject-card:focus-visible .subject-card-info,
  .subject-card:focus-within .subject-card-info {
    transform: translateX(-50%);
  }
}

.subject-card .subject-free-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  background: linear-gradient(180deg, #4ce694, var(--kais-green, #58CC02));
  box-shadow:
    0 4px 12px -4px rgba(47, 211, 122, .9),
    inset 0 1px 0 rgba(255, 255, 255, .45);
  color: #06301c;
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.18rem 0.5rem;
  border-radius: var(--radius-pill, 999px);
  line-height: 1;
  z-index: 3;
}

.subject-card.is-free {
  border-color: color-mix(in srgb, var(--kais-green, #58CC02) 40%, var(--border-card));
}

/* Visually-hidden helper text — readable by screen readers, invisible
   on screen. Standard a11y utility used by the Free badge. */
.sr-only {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* An .sr-only container that HOLDS A FOCUSABLE CHILD is a Focus Visible
   (WCAG 2.4.7) hazard, not just a styling curiosity: `clip` suppresses paint
   but not focusability, so `#products-heading` — a visually-hidden span
   wrapping a real <a href="/fixseat/"> — took a tab stop that a sighted
   keyboard user could not see anywhere on screen (MEASURED: 70x23 CSS px of
   live layout, painted nowhere, UA ring at 1.09:1).

   Reveal the container while anything inside it has focus, same pattern the
   skip link uses. `:focus-within` cannot match on an .sr-only span with no
   focusable content, so the Free-badge helper text is unaffected. This also
   settles SC 2.5.8 for that link — the revealed chip is well over 24x24. */
.kais-home .sr-only:focus-within {
  position: fixed !important;
  inset-block-start: 1.25rem;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  z-index: 9999;
  width: auto;
  height: auto;
  margin: 0;
  padding: 0.75rem 1rem;
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: normal;
  background: var(--bg-card);
  color: var(--text-heading);
  border: 2px solid var(--brand);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-card);
}

.kais-home .sr-only:focus-within a {
  color: var(--accent-ink, var(--brand));
  font-weight: 700;
}

/* 8. How It Works
   ------------------------------------------------------------ */
.steps-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  margin-top: clamp(2.5rem, 4vw, 3.25rem);
  position: relative;
}

@media (min-width: 900px) {
  .steps-row {
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
  }

  /* Connecting line between steps */
  .steps-row::before {
    content: "";
    position: absolute;
    top: 48px;
    left: calc(16.67% + 24px);
    right: calc(16.67% + 24px);
    height: 2px;
    background: repeating-linear-gradient(
      90deg,
      rgba(155, 109, 255, .35) 0px,
      rgba(155, 109, 255, .35) 8px,
      transparent 8px,
      transparent 16px
    );
    z-index: 0;
  }
}

.step-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 2rem 1.5rem;
  position: relative;
  z-index: 1;
  overflow: hidden;
  transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease;
}

.step-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--brand), var(--brand-2));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s ease;
  z-index: 2;
}

.step-card:hover::before {
  transform: scaleX(1);
}

/* Oversized ghost numeral watermark, driven by `data-step` on the
   article so the number is never duplicated in the accessibility tree
   (CSS `content` is decorative). Gives each step card a piece of
   character without adding a single word of copy. */
.step-card::after {
  content: attr(data-step);
  position: absolute;
  top: -0.35em;
  inset-inline-end: 0.1em;
  z-index: 0;
  pointer-events: none;
  font-size: 8rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.05em;
  color: var(--ghost-ink, rgba(255, 255, 255, .035));
  transition: color .35s ease, transform .5s var(--ease-out);
}

.step-card:hover::after {
  color: var(--ghost-ink-hover, rgba(255, 255, 255, .06));
  transform: translateY(4px);
}

.step-card > * {
  position: relative;
  z-index: 1;
}

.step-card:hover {
  transform: translateY(-6px) scale(1.02);
  box-shadow: var(--shadow-card-hover), var(--glow-blue), var(--inner-top-light);
}

.step-number-badge {
  width: 56px;
  height: 56px;
  border-radius: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  font-weight: 800;
  /* DARK ink, not white. MEASURED (median pixel) with the previous
     `color: #fff` on the light-topped gradients below:
       blue 2.40:1 · purple 3.35:1 · green 1.94:1
     The digit is 24px/800, so WCAG only asks 3:1 — two of the three missed
     even that. Dimming the gradients to carry white would have cost the
     brand hues their saturation; inking the numeral instead keeps them at
     full strength and lands at 5.4-9.7:1. It is also the treatment
     `.pricing-badge` (#0a1020) and `.subject-free-badge` (#06301c) already
     use, so the three badge families now read as one family. */
  color: #0b1020;
  margin-bottom: 1.25rem;
  flex-shrink: 0;
  box-shadow:
    0 10px 20px -6px rgba(106, 168, 255, .45),
    inset 0 1px 0 rgba(255, 255, 255, .45),
    inset 0 -10px 16px -10px rgba(0, 0, 0, .55);
  transition: transform 0.4s var(--ease-spring), box-shadow .3s ease;
}

.step-card:hover .step-number-badge {
  transform: scale(1.15) rotate(5deg);
  box-shadow:
    0 16px 28px -8px rgba(106, 168, 255, .6),
    inset 0 1px 0 rgba(255, 255, 255, .55),
    inset 0 -10px 16px -10px rgba(0, 0, 0, .55);
}

/* CSP-N05 follow-up: per-step accent colors, replacing the previous
   inline ``style="background: var(--kais-*);"`` attributes on each
   step badge in templates/app/home.html.

   Each ramp is now clamped at its DARK end so the #0b1020 numeral clears
   4.5:1 against EVERY stop, not just the midpoint — the previous ramps ran
   on down to #3d7fd6 / #6d3ecc / #17945a, which left the bottom of the chip
   darker than the ink sitting on it. The bright ends are untouched, so the
   chips read exactly as vividly as before. */
.step-number-badge--blue {
  background: linear-gradient(150deg, #a8d0ff, var(--kais-blue) 60%, #4b8ae6);
}

.step-number-badge--purple {
  background: linear-gradient(150deg, #d3bcff, #b18cff 60%, var(--kais-purple));
}

.step-number-badge--green {
  background: linear-gradient(150deg, #8ff5c4, #4ce694 60%, var(--kais-green));
}

.step-card-title {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--text-heading);
  margin: 0 0 0.5rem;
}

.step-card-desc {
  font-size: 0.95rem;
  color: var(--text-body);
  line-height: 1.6;
  margin: 0;
}

/* 8b. Keduka School OS band (#school-os)
   ------------------------------------------------------------
   Semantic classes, not Tailwind utilities: `tailwind-main.css` is
   vendored and pre-built with no pipeline, so an arbitrary utility here
   would silently do nothing — and because `mx-auto` IS in the bundle
   while `max-w-*` is not, the failure reads as an unstyled page rather
   than as an error.

   `.school-os-card` is listed on the shared card-surface rule at the top
   of this file, so the glass wash, the border, the shadow and the
   `@supports not (backdrop-filter)` opaque fallback all come from one
   place rather than being re-declared here.

   The CARD rules below introduce no new colour — every token they use
   already exists. The BAND rule that follows is the one exception in
   this block: `.school-os-section` declares two literals, #080c1c and
   #060912, because it is deliberately RECESSED below `--bg-page`
   (#0b1020) and no existing token is darker than the page itself. They
   are values of the same navy, not a new hue, and the contrast they
   govern is MEASURED rather than assumed — see the note on that rule. */

/* The band itself. `.kais-section` is padding-only and declares NO
   background, so #school-os and its bare-`.kais-section` neighbour
   #how-it-works used to stack two 7rem paddings with nothing between
   them — a ~190px stretch with no rule, no colour change and no reason
   for the eye to stop. It read as a rendering bug, not as a new band.

   Deliberately NOT a second `.kais-section-alt`: that one is a FLAT,
   semi-transparent navy slab (rgba(14,20,41,.72)) that sits LIGHTER than
   the page and seams in white, and #pricing already wears it two bands
   down the page. This is the one B2B interstitial on a consumer page, so
   it gets the inverse treatment — a RECESSED, brand-lit well:

     * darker than both neighbours (#showcase is opaque --bg-page,
       #how-it-works is transparent), so the boundary is a step DOWN
       rather than another wash of the same navy;
     * brand light bleeding in from two corners rather than a flat tint,
       reusing the exact `--gradient-hero` idiom (the same two rgba forms
       of --kais-blue / --kais-purple, no new hue);
     * brand-gradient hairline seams instead of the white ones.

   Both radial cores are anchored OFF-canvas (-22% above, 122% below) and
   are fully transparent by 58%, so the bright part of each never reaches
   the centred content column. That is a CONTRAST decision, not a
   stylistic one: the column composites to the base slab, which measures
   DARKER than the #0b1020 the section previously showed through, so
   `.school-os-note` on --text-muted-strong reads ~6.4:1 here against
   ~6.2:1 before — the wash cannot dim the copy it sits behind.

   MEASURED, computed from the literal token values rather than eyeballed:
   --text-muted-strong #8a93b2 on the band base is 6.39:1 at #080c1c and
   6.53:1 at #060912, against 6.22:1 on the old --bg-page. The figure that
   actually bounds the risk is the WORST case — if a radial core were ever
   moved on-canvas and landed at full strength directly beneath the copy,
   it still measures 5.01:1 (blue) and 5.19:1 (purple). So this band clears
   AA 4.5:1 even if the off-canvas anchoring above is later edited away.
   Re-measure if any of those four values changes. */
.school-os-section {
  /* Tighter than the section default. This is an interstitial (one
     heading, three short cards, two notes, one CTA), not a peer of
     #subjects or #pricing, and it now abuts #showcase's own bottom
     padding — inheriting the full 7rem would re-create on that seam the
     very stack this band exists to break. */
  padding-block: clamp(3.25rem, 6vw, 5.5rem);
  background: var(--school-os-surface,
    radial-gradient(65% 90% at 6% -22%, rgba(106, 168, 255, .16), transparent 58%),
    radial-gradient(60% 85% at 94% 122%, rgba(155, 109, 255, .18), transparent 58%),
    linear-gradient(180deg, #080c1c, #060912));
}

/* Same "considered edge" language as the `.kais-section-alt` seams — a
   1px line that fades out at both ends — carrying brand colour rather
   than white, so the two treatments are siblings and not twins.
   Decorative: no content, no pointer target. */
.school-os-section::before,
.school-os-section::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  height: 1px;
  background: linear-gradient(90deg,
              transparent,
              rgba(106, 168, 255, .38) 22%,
              rgba(155, 109, 255, .38) 78%,
              transparent);
  pointer-events: none;
}

.school-os-section::before { top: 0; }
.school-os-section::after { bottom: 0; }

/* The band illustration, between the heading and the three claim cards.
   Transparent WebP authored for a dark ground with border alpha 0, so it
   needs no plate, no border and no background of its own — a seam is
   impossible by construction rather than by matching a hex. That matters
   more here than on /teams/: this band is the one surface in the file that
   is DARKER than --bg-page, so an asset carrying even a faint opaque edge
   would betray itself against #080c1c where it passed against #0b1020.

   No contrast floor applies to it. WCAG 1.4.11 governs graphics you need in
   order to understand the content; this one is aria-hidden and restates the
   three cards beneath it, so it is exempt by being genuinely decorative
   rather than by being excused. The MEASURED figures on the band rule above
   are unaffected — the art sits between the heading and the cards and never
   underlaps the copy, so nothing composites on top of it.

   Capped rather than stretched. The band is 16:9 and the container runs to
   1140px (1320px above 1600px), so past ~60rem the illustration stops being
   a divider between heading and cards and starts being a hero of its own. */
.school-os-art {
  display: block;
  inline-size: 100%;
  max-inline-size: 60rem;
  margin-inline: auto;
  margin-block-start: clamp(2rem, 3.5vw, 2.75rem);
}

.school-os-art-img {
  display: block;
  inline-size: 100%;
  /* The intrinsic 16:9 width/height on the element reserves the box before
     the bytes arrive; `block-size: auto` hands the ratio back to the image
     once CSS owns the width. Without it a lazy-loaded asset this far down
     the page would collapse to zero height and snap the cards upward at the
     moment the visitor scrolls it into view. */
  block-size: auto;
}

.school-os-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem;
  margin-top: clamp(2.5rem, 4vw, 3.25rem);
}

@media (min-width: 900px) {
  .school-os-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 1.5rem;
  }
}

.school-os-card {
  padding: 1.75rem var(--card-padding-x);
  position: relative;
  overflow: hidden;
  transition: transform 0.4s var(--ease-out), box-shadow 0.3s ease;
}

/* Top-accent seam, mirroring `.step-card::before` so the two adjacent
   bands read as one system. Decorative only. */
.school-os-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--gradient-cta);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s ease;
}

.school-os-card:hover::before {
  transform: scaleX(1);
}

.school-os-card:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow-card-hover);
}

.school-os-card-title {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--text-heading);
  margin: 0 0 0.5rem;
}

.school-os-card-desc {
  font-size: 0.95rem;
  color: var(--text-body);
  line-height: 1.6;
  margin: 0;
}

/* The add-on caveat and the non-numeric pricing line. `--text-muted-strong`
   rather than `--text-muted`: this is small copy carrying a material fact
   about what a seat includes, so it needs the 5.6:1 token, not the
   4.56:1 one. */
.school-os-note {
  max-width: 60ch;
  margin: 1.5rem auto 0;
  text-align: center;
  font-size: 0.92rem;
  line-height: 1.65;
  color: var(--text-muted-strong);
}

.school-os-actions {
  margin-top: 2rem;
}

/* 44px minimum tap target on phones, where this is the only route from
   the home page to the organizational surface. */
.school-os-actions .btn-kais-primary,
.school-os-actions .btn-kais-ghost {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

@media (prefers-reduced-motion: reduce) {
  .school-os-card {
    transition: none;
  }

  .school-os-card:hover {
    transform: none;
  }

  .school-os-card::before {
    transition: none;
  }
}

/* Teacher mini-list inside step 3 */
.teacher-mini-list {
  list-style: none;
  padding: 0;
  margin: 1rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  text-align: left;
  width: 100%;
}

.teacher-mini-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.88rem;
  color: var(--text-body);
}

.teacher-mini-list li i {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  color: #fff;
  flex-shrink: 0;
}

/* 9. Pricing Preview
   ------------------------------------------------------------ */

/* Add-on disclosure ($15/mo agent chat + dokai).
   Deliberately calm and secondary — it must be legible and findable, not
   a sales pitch — but it must NOT be visually suppressed either: the whole
   point is that the page previously said nothing about these purchases
   while headlining "$10 a month / Every subject unlocked". Body-colour
   text on the card surface, never the faint muted token. */
.pricing-addons {
  max-width: 32rem;
  margin: 2rem auto 0;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--border-card);
  border-radius: var(--card-radius);
  background: var(--panel-veil, rgba(255, 255, 255, .03));
  text-align: start;
}

.pricing-addons-heading {
  margin: 0 0 0.35rem;
  font-size: 1rem;
  font-weight: 700;
  color: var(--text-heading);
}

.pricing-addons-note {
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-body);
}

.pricing-addons-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.pricing-addons-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.9rem;
  color: var(--text-body);
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border-light);
}

.pricing-addons-list li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.pricing-addon-name {
  font-weight: 600;
  color: var(--text-heading);
}

.pricing-addon-price {
  font-weight: 700;
  color: var(--accent-ink, var(--brand));
  white-space: nowrap;
}

.pricing-addons-currency {
  margin: 0.85rem 0 0;
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--text-muted-strong);
}

.pricing-grid-home {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
  margin-top: 2.5rem;
  max-width: 720px;
  margin-inline: auto;
}

@media (min-width: 640px) {
  .pricing-grid-home {
    grid-template-columns: repeat(2, 1fr);
  }
}

.pricing-card-home {
  border-width: 1px;
  padding: 2rem 1.5rem;
  display: flex;
  flex-direction: column;
  transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease;
  position: relative;
  overflow: hidden;
}

.pricing-card-home::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--brand), var(--brand-2));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s ease;
  z-index: 2;
}

.pricing-card-home:hover::before {
  transform: scaleX(1);
}

.pricing-card-home:hover {
  transform: translateY(-6px) scale(1.02);
  box-shadow: var(--shadow-card-hover), var(--glow-blue), var(--inner-top-light);
}

.pricing-card-home.popular {
  border-color: rgba(106, 168, 255, .55);
  /* Halo so the recommended plan is obvious before you read a word of
     it. Overflow is hidden on the card, so the halo has to be an outer
     shadow rather than a glow layer. */
  box-shadow:
    0 0 0 1px rgba(106, 168, 255, .25),
    0 26px 60px -30px rgba(106, 168, 255, .95),
    var(--inner-top-light);
}

.pricing-card-home.popular::before {
  /* The recommended card wears its accent bar permanently. */
  transform: scaleX(1);
}

.pricing-badge {
  position: absolute;
  top: -12px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--gradient-cta);
  color: #0a1020;
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.3rem 0.9rem;
  border-radius: var(--radius-pill);
  box-shadow:
    0 8px 20px -8px rgba(106, 168, 255, .95),
    inset 0 1px 0 rgba(255, 255, 255, .5);
}

.pricing-plan-name {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--text-heading);
  margin: 0 0 0.25rem;
}

.pricing-price-row {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
  margin: 0.75rem 0 0.5rem;
}

.pricing-amount {
  font-size: 2.5rem;
  font-weight: 800;
  background: var(--gradient-cta-ink, var(--gradient-cta));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  line-height: 1;
}

.pricing-period {
  font-size: 1rem;
  color: var(--text-muted);
}

.pricing-desc {
  font-size: 0.9rem;
  color: var(--text-body);
  margin: 0.25rem 0 1rem;
  line-height: 1.5;
}

.pricing-features-list {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  flex: 1;
}

.pricing-features-list li {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  /* Room for the drawn tick disc rendered by ::before. */
  padding-inline-start: 1.75rem;
  font-size: 0.9rem;
  color: var(--text-body);
  line-height: 1.4;
}

/* Feature tick, drawn entirely in CSS.
   The markup carries `<i class="fas fa-check-circle">`, but NO Font
   Awesome stylesheet is loaded anywhere in this project — every `fas`
   glyph resolves to nothing. Styling that <i> into a tinted disc would
   therefore ship a column of EMPTY circles. So the icon element is
   dropped from layout and the tick is drawn from a rotated border,
   which renders identically with or without an icon font. */
.pricing-features-list li i {
  display: none;
}

.pricing-features-list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.05rem;
  width: 1.2rem;
  height: 1.2rem;
  border-radius: 50%;
  background: rgba(47, 211, 122, .14);
  box-shadow: inset 0 0 0 1px rgba(47, 211, 122, .28);
}

.pricing-features-list li::after {
  content: "";
  position: absolute;
  inset-inline-start: 0.44rem;
  top: 0.3rem;
  width: 0.3rem;
  height: 0.55rem;
  border: solid var(--ok);
  border-width: 0 2px 2px 0;
  /* Rotation-only, so the tick is identical under RTL — the position
     flips via inset-inline-start, the glyph itself must not mirror. */
  transform: rotate(45deg);
}

.pricing-card-home .btn-kais-primary,
.pricing-card-home .btn-kais-ghost {
  width: 100%;
  justify-content: center;
}

/* Stripe checkout container */
#checkout {
  margin-top: 2rem;
  border-radius: var(--radius-card);
  overflow: hidden;
}

#checkout:empty {
  display: none;
}

/* Billing portal links */
.billing-links {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-top: 1.5rem;
  flex-wrap: wrap;
}

.billing-links a {
  color: var(--accent-ink, var(--brand));
  font-size: 0.9rem;
  text-decoration: none;
  font-weight: 600;
  transition: color 0.2s ease;
}

.billing-links a:hover {
  color: var(--accent-2-ink, var(--brand-2));
  text-decoration: underline;
}

/* 9b. Pricing comparison band ("How KAIS compares")
   ------------------------------------------------------------
   Tokens-only value band. Carries .reveal-item, so it inherits the
   opacity:0 baseline + IO .visible fallback + the reduced-motion
   opacity:1 pin already defined in this file. */
.pricing-compare {
  position: relative;
  max-width: 720px;
  margin: clamp(2.5rem, 4vw, 3.25rem) auto 0;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(155, 109, 255, .13), transparent 62%),
    var(--bg-card);
  border: 1px solid var(--border-card);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card), var(--inner-top-light);
  padding: clamp(1.25rem, 4vw, 2rem);
}

.pricing-compare-heading {
  font-size: clamp(1.3rem, 3vw, 1.6rem);
  font-weight: 700;
  color: var(--text-heading);
  text-align: center;
  margin: 0 0 0.35rem;
  line-height: 1.2;
}

.pricing-compare-subhead {
  font-size: 0.95rem;
  /* --text-muted is a 4.56:1 pass on FLAT --bg-card, but `.pricing-compare`
     lays a rgba(155,109,255,.13) radial wash over that card and this subhead
     sits directly under the wash's brightest point: MEASURED 3.98:1 against
     a 4.5:1 requirement. --text-muted-strong measures 4.9:1 on the same
     pixels. The sibling `.pricing-compare-disclaimer` already documents this
     hazard — same card, same reason. */
  color: var(--text-muted-strong);
  text-align: center;
  max-width: 48ch;
  margin: 0 auto 1.75rem;
  line-height: 1.5;
}

.pricing-compare-bars {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

/* Mobile-first: name + price on the top line, track full-width below. */
.pricing-compare-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "name price"
    "track track";
  align-items: center;
  column-gap: 0.75rem;
  row-gap: 0.4rem;
  padding: 0.4rem 0.55rem;
  border-radius: 12px;
}

.pricing-compare-name {
  grid-area: name;
  min-width: 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-body);
  line-height: 1.3;
}

.pricing-compare-track {
  grid-area: track;
  height: 16px;
  /* Recessed groove: an inset shadow reads as a channel the bar sits
     IN, rather than a grey bar sitting next to it. */
  background: var(--track-surface, rgba(0, 0, 0, .28));
  box-shadow: inset 0 1px 3px var(--track-inset, rgba(0, 0, 0, .55));
  border-radius: var(--radius-pill);
  overflow: hidden;
}

/* BASELINE = FINAL width so reduced-motion / no-JS users always see full
   bars. GSAP animates scaleX 0->1 on top; a scaleX(0) baseline would trap
   the bars invisible without JS. */
.pricing-compare-fill {
  width: var(--bar-target, 100%);
  height: 100%;
  transform-origin: left center;
  border-radius: var(--radius-pill);
  /* Top gloss baked into the same background stack rather than a
     pseudo-element: GSAP scales this element on scaleX, and a child
     pseudo would be squashed along with it mid-tween. */
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .22), transparent 60%),
    linear-gradient(90deg, var(--text-muted), var(--text-body));
}

.pricing-compare-price {
  grid-area: price;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-end;
  gap: 0.4rem;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text-heading);
  text-align: right;
}

/* KAIS row — the highlight. Accent fill + subtle ring/tint + bolder type. */
.pricing-compare-row.is-kais {
  background: color-mix(in srgb, var(--brand-2) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand-2) 30%, transparent);
}

.pricing-compare-row.is-kais .pricing-compare-fill {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .28), transparent 60%),
    var(--gradient-cta);
  box-shadow: 0 0 16px -2px rgba(106, 168, 255, .8);
}

.pricing-compare-row.is-kais .pricing-compare-name {
  color: var(--text-heading);
  font-weight: 700;
}

.pricing-compare-ribbon {
  display: inline-block;
  /* Solid dark accent, not --gradient-cta: white on the gradient is only
     2.4-3.5:1. White on --brand-accent-strong is 5.7:1 (AA small text). */
  background: var(--brand-accent-strong);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.3;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-pill);
}

.pricing-compare-savings {
  max-width: 54ch;
  margin: 1.75rem auto 0;
  text-align: center;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--text-body);
}

.pricing-compare-save-figure {
  display: inline-block;
  font-size: 1.6rem;
  font-weight: 800;
  background: var(--gradient-cta-ink, var(--gradient-cta));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  vertical-align: baseline;
}

.pricing-compare-disclaimer {
  max-width: 62ch;
  margin: 1.25rem auto 0;
  text-align: center;
  font-size: 0.72rem;
  line-height: 1.5;
  /* Smallest copy in the band -> use the stronger muted token for a
     comfortable AA margin (5.6:1) instead of --text-muted's thin 4.56:1. */
  color: var(--text-muted-strong);
}

/* Screen-reader data table for the decorative bar chart — visually hidden
   (clip-rect / clip-path pattern), readable by assistive tech.

   `table-layout: fixed` is LOAD-BEARING, not cosmetic. A CSS table's used
   width is max(specified, min-content), so under the default `auto` layout
   the `width: 1px` above was silently ignored and the table laid out at its
   full ~549px min-content width (`white-space: nowrap` makes every row
   unbreakable). `clip`/`clip-path` only suppress PAINT — the layout box
   still counted toward document scroll width, so the home page carried a
   horizontal scrollbar at every phone width. `fixed` makes the declared
   1px authoritative, and `white-space` is reset to `normal` so the cells
   can actually wrap down to that width — with `nowrap` every row stays one
   unbreakable line and the table re-expands regardless of table-layout.
   Measured: document scrollWidth 593px -> 390px at a 390px viewport. */
.chart-sr-table {
  position: absolute;
  table-layout: fixed;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: normal;
  border: 0;
}

/* Desktop: name | track | price on one aligned row. Fixed side columns
   keep the tracks aligned across the independent per-row grids. */
@media (min-width: 640px) {
  .pricing-compare-row {
    grid-template-columns: 8.5rem 1fr 6.5rem;
    grid-template-areas: "name track price";
    column-gap: 1rem;
    row-gap: 0;
  }
}

/* 10. FAQ
   ------------------------------------------------------------ */
.faq-section-narrow {
  max-width: 720px;
  margin-inline: auto;
}

.faq-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: clamp(2.5rem, 4vw, 3.25rem);
}

.faq-item {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
  transition: border-color 0.2s ease, box-shadow 0.2s ease,
              transform 0.25s var(--ease-out);
}

.faq-item:hover {
  border-color: var(--stroke-strong);
  transform: translateX(2px);
}

/* Accent spine on the open item. Uses the inline-start edge so it
   flips correctly under RTL. */
.faq-item::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  pointer-events: none;
  background: var(--gradient-cta);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform .3s var(--ease-out);
}

.faq-item[open]::before {
  transform: scaleY(1);
}

.faq-item[open] {
  border-color: rgba(106, 168, 255, .45);
  box-shadow: 0 10px 30px -18px rgba(106, 168, 255, .9), var(--inner-top-light);
}

.faq-item summary {
  padding: 1.1rem 1.25rem;
  font-weight: 600;
  font-size: 1rem;
  color: var(--text-heading);
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

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

/* Chevron in its own disc, so the affordance is a real target-looking
   control rather than a loose glyph floating at the end of the row. */
.faq-item summary::after {
  content: "\25BE";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--chip-veil-strong, rgba(255, 255, 255, .05));
  box-shadow: inset 0 0 0 1px var(--stroke-soft);
  font-size: 0.95rem;
  line-height: 1;
  color: var(--text-muted);
  flex-shrink: 0;
  transition: transform 0.25s var(--ease-spring), color 0.2s ease,
              background 0.2s ease;
}

.faq-item summary:hover::after {
  background: rgba(106, 168, 255, .14);
  color: var(--accent-ink, var(--brand));
}

.faq-item[open] summary::after {
  transform: rotate(180deg);
  color: var(--accent-ink, var(--brand));
  background: rgba(106, 168, 255, .16);
}

.faq-item summary:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: -3px;
  border-radius: 14px;
}

.faq-answer {
  padding: 0 1.25rem 1.1rem;
  font-size: 0.95rem;
  color: var(--text-body);
  line-height: 1.7;
}

/* 11. Final CTA
   ------------------------------------------------------------ */
.final-cta-section {
  background: radial-gradient(600px 300px at 20% -10%, rgba(106, 168, 255, .2), transparent),
              radial-gradient(600px 300px at 90% 10%, rgba(155, 109, 255, .2), transparent);
  padding-block: clamp(4rem, 7vw, 6.5rem);
  text-align: center;
  position: relative;
  overflow: hidden;
}

.final-cta-section > * {
  position: relative;
  z-index: 1;
}

/* Texture + a focused pool of light behind the closing pitch.
   z-index 1 puts it above the photo backdrop and its scrim (both
   z-index 0); the real content is also z-index 1 but comes later in
   DOM order, so it still paints on top. `pointer-events: none` keeps
   the final CTA button clickable through the layer. */
.final-cta-section::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image:
    radial-gradient(520px 300px at 50% 40%, rgba(155, 109, 255, .18), transparent 70%),
    var(--dot-grid);
  background-size: auto, var(--dot-grid-size);
  -webkit-mask-image: radial-gradient(90% 80% at 50% 50%, #000 20%, transparent 80%);
  mask-image: radial-gradient(90% 80% at 50% 50%, #000 20%, transparent 80%);
}

.final-cta-section h2 {
  font-size: clamp(1.75rem, 3.5vw, 2.5rem);
  font-weight: 800;
  color: var(--text-heading);
  letter-spacing: -0.025em;
  text-wrap: balance;
  margin: 0 0 0.75rem;
}

.final-cta-section p {
  font-size: clamp(1rem, 2vw, 1.15rem);
  color: var(--text-body);
  margin: 0 0 1.75rem;
  line-height: 1.6;
}

.final-cta-section .btn-kais-primary {
  box-shadow: 0 15px 24px rgba(106, 168, 255, .3);
}

.final-cta-section .btn-kais-primary:hover {
  box-shadow: 0 20px 30px rgba(106, 168, 255, .4);
}

/* 12. Reveal-on-Scroll Animations
   ------------------------------------------------------------ */
.reveal-item {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.reveal-item.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Staggered delays for grid children */
.reveal-item:nth-child(2) { transition-delay: 0.08s; }
.reveal-item:nth-child(3) { transition-delay: 0.16s; }
.reveal-item:nth-child(4) { transition-delay: 0.24s; }
.reveal-item:nth-child(5) { transition-delay: 0.32s; }
.reveal-item:nth-child(6) { transition-delay: 0.40s; }

/* Hero fade-in */
.hero-text {
  opacity: 0;
  transform: translateY(20px);
  animation: hero-fade-in 0.7s ease 0.2s forwards;
}

.hero-visual {
  opacity: 0;
  animation: hero-fade-in 0.7s ease 0.5s forwards;
}

@keyframes hero-fade-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* 13. Responsive
   ------------------------------------------------------------ */
@media (max-width: 639px) {
  .hero-heading {
    font-size: 2rem;
  }

  .hero-actions {
    flex-direction: column;
  }

  .hero-actions .btn-kais-primary,
  .hero-actions .btn-kais-ghost {
    width: 100%;
  }

  .hero-proof {
    gap: 1rem;
  }

  /* Phone height budget: the price block must stay compact enough that
     it AND the CTA row clear the fold on a 390x844 viewport. Tighten
     padding + caption type; the 3-column tile grid is deliberately NOT
     relaxed to a wrap here (a second tile row is exactly what would
     push the CTA off screen). */
  .hero-price {
    padding: 0.75rem 0.85rem;
    max-width: none;
  }

  .hero-price-tiles {
    gap: 0.35rem;
  }

  .hero-price-tile {
    padding-inline: 0.45rem;
  }

  .hero-price-caption {
    font-size: 0.65rem;
  }

  .hero-price-claim {
    margin-top: 0.6rem;
    padding-top: 0.55rem;
    font-size: 0.78rem;
  }

  .subjects-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 0.75rem;
  }

  .subject-card {
    padding: 1rem 0.5rem;
  }

  .subject-card i {
    width: 40px;
    height: 40px;
    font-size: 1.1rem;
  }

  .subject-card > .subject-card-name {
    font-size: 0.78rem;
  }

  /* Touch has no hover: the hover tooltip would only ever overlap the grid,
     so hide it below the phone breakpoint — the card stays a plain link. */
  .subject-card-info {
    display: none;
  }
}

@media (max-width: 479px) {
  .subjects-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Smallest-phone fold budget (<= 359px, i.e. 320x568 class devices)
   ------------------------------------------------------------------
   MEASURED (tests/visual/home_fold_check.py): at 320x568 `.hero-price`
   landed at bottom=608 against a 568px fold — 40px over — while all 16
   other viewports passed. The overshoot is entirely in the chrome
   ABOVE the price:

     badge     54px  (the label wraps to two lines at this width)
     subtitle 128px  (five lines at 16px/1.6)
     gaps      52px  (3 x 16px stack gap + the price block's margin)
     section   24px  (hero padding-top)

   So the compaction targets exactly those, and nothing below the
   price. The 639px phone breakpoint is far too wide to carry this —
   applying these sizes to a 390px phone would over-shrink a viewport
   that already passes with 260px of headroom.

   No content is withheld from this viewport: every element still
   renders, just tighter. */
@media (max-width: 359px) {
  .hero-section {
    padding-top: 0.6rem;
  }

  .hero-text {
    gap: 0.7rem;
  }

  .hero-badge {
    padding: 0.3rem 0.7rem;
    font-size: 0.66rem;
    letter-spacing: 0.03em;
    line-height: 1.2;
  }

  .hero-heading {
    font-size: 1.8rem;
  }

  .hero-subtitle {
    font-size: 0.88rem;
    line-height: 1.5;
  }

  .hero-price {
    padding: 0.7rem 0.75rem;
  }

  .hero-price-amount {
    font-size: 1.25rem;
  }

  .hero-price-caption {
    font-size: 0.6rem;
    line-height: 1.3;
  }

  .hero-price-claim {
    margin-top: 0.5rem;
    padding-top: 0.45rem;
    font-size: 0.72rem;
    line-height: 1.4;
  }
}

/* Short viewports (landscape phone, ~390px tall). Width-based breakpoints
   can't see this case at all — a 844x390 landscape phone is "desktop wide"
   and "shorter than a phone is tall" at the same time. Without a
   height-keyed rule the price block measured 30px past the fold there.
   Compress the chrome above the price so the number still lands on
   screen; the subtitle shrinks rather than disappearing, so no content is
   withheld from this viewport. */
@media (max-height: 520px) and (orientation: landscape) {
  .hero-section {
    padding-top: 0.75rem;
    padding-bottom: 1.5rem;
  }

  .hero-text {
    gap: 0.6rem;
  }

  .hero-badge {
    padding: 0.25rem 0.7rem;
    font-size: 0.7rem;
  }

  .hero-heading {
    font-size: 1.75rem;
  }

  .hero-subtitle {
    font-size: 0.85rem;
    line-height: 1.45;
  }

  .hero-price {
    padding: 0.6rem 0.8rem;
  }

  .hero-price-amount {
    font-size: 1.25rem;
  }

  .hero-price-caption {
    font-size: 0.62rem;
  }

  .hero-price-claim {
    margin-top: 0.45rem;
    padding-top: 0.45rem;
    font-size: 0.72rem;
  }

  .hero-actions {
    margin-top: 0;
  }

  .hero-actions .btn-kais-primary,
  .hero-actions .btn-kais-ghost {
    padding-block: 0.55rem;
  }
}

/* Floating subject cards: hide on phones (decorative, aria-hidden), shrink on
   tablet/cramped-laptop so they fit inside the narrower image column. */
@media (max-width: 639px) {
  .hero-visual .hero-float-card {
    display: none;
  }
}

@media (min-width: 640px) and (max-width: 1023px) {
  .hero-float-card {
    padding: 0.6rem 0.85rem;
    font-size: 0.8rem;
    gap: 0.5rem;
  }

  .hero-float-icon {
    width: 30px;
    height: 30px;
    font-size: 1rem;
    border-radius: 9px;
  }

  .hero-float-card:nth-child(1) {
    top: 6%;
    left: 4%;
  }

  .hero-float-card:nth-child(2) {
    top: 32%;
    right: 4%;
  }

  .hero-float-card:nth-child(3) {
    bottom: 18%;
    left: 4%;
  }

  .hero-float-card:nth-child(4) {
    bottom: 6%;
    right: 4%;
  }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  /* `html { scroll-behavior: smooth }` at the top of this file animates EVERY
     in-page jump, and the hero now ships three of them: the global skip link,
     `.hero-price-link` -> #pricing, and `.btn-kais-ghost` -> #how-it-works.
     MEASURED under `reduced_motion: reduce`, the computed scroll-behavior on
     <html> was still `smooth`, so a reader who asked for no motion got a
     full-page scroll animation from a link labelled "See the full
     comparison". */
  html {
    scroll-behavior: auto;
  }

  .reveal-item {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .hero-text,
  .hero-visual {
    opacity: 1;
    transform: none;
    animation: none;
  }

  /* Inherited from tailwind-main.css, which animates EVERY h1 site-wide
     with `fadeInDown` and offers no reduced-motion escape. MEASURED: the
     hero h1 — the page's LCP element — was still sliding 40px and fading
     in for reduced-motion users. Disabling it here is safe because the
     keyframe ENDS at opacity 1 / translateY(0) and the element's own
     baseline is already opaque, so removing the animation reveals it
     rather than hiding it. Scoped to this page; the global rule is in a
     shared stylesheet with a site-wide blast radius. */
  .hero-heading {
    animation: none;
    opacity: 1;
    transform: none;
  }

  /* New cinematic badge — disable enter, glow, shimmer.
     The base class alone is NOT enough: the shimmer lives on the `::before`
     pseudo-element, which a bare `.hero-badge-animated` rule does not reach.
     WCAG 2.3.3 (Animation from Interactions) requires the pseudo to stop too,
     otherwise the gradient sweep keeps sliding for reduced-motion users. */
  .hero-badge-animated {
    opacity: 1;
    transform: none;
    animation: none !important;
  }

  .hero-badge-animated::before {
    animation: none !important;
    background: none;
  }

  /* Staggered hero lines — instant visible state. `.gsap-word` is in
     the list belt-and-braces: the loader never injects GSAP under
     reduced motion, so nothing normally splits the heading, but a word
     span that somehow exists must not be left shimmering. */
  .hero-line,
  .hero-line--primary,
  .hero-line--secondary,
  .hero-line .gsap-word {
    opacity: 1;
    transform: none;
    animation: none !important;
    filter: none;
  }

  .hero-float-card {
    animation: none;
  }

  .hero-section::before,
  .hero-section::after {
    animation: none;
  }

  /* Design-system layer: every ambient/decorative animation added by
     the 2026-07 visual pass. Each of these is purely atmospheric, so
     switching it off costs the user no information — the aurora, the
     orb drift and the price-card sheen all resolve to a static,
     fully-legible resting state.

     The tail of the `transition: none` list below covers the hover
     transitions this block originally MISSED. MEASURED with
     `reduced_motion: reduce`, each of those selectors still reported a
     non-zero transition-duration, so its motion still played: the step
     badge's `scale(1.15) rotate(5deg)` (new in this pass), the card
     families' `translateY(-6px) scale(1.02)` lift, the accent bars'
     `scaleX(0 -> 1)` wipe, and both button styles' lift + cross-fade.
     Only the TRANSITIONS are dropped — the hover end-states still apply,
     so every affordance survives and simply arrives instantly. */
  .hero-fx::before {
    animation: none;
  }

  .hero-price::after {
    animation: none;
  }

  .subject-card::after,
  .subject-card .subject-icon,
  .step-card::after,
  .faq-item,
  .faq-item::before,
  .faq-item summary::after,
  .hero-float-card,
  .hero-actions .btn-kais-primary::after,
  .step-card,
  .step-card::before,
  .step-number-badge,
  .subject-card,
  .subject-card::before,
  .pricing-card-home,
  .pricing-card-home::before,
  .btn-kais-primary,
  .btn-kais-primary::before,
  .btn-kais-ghost {
    transition: none;
  }

  /* Bars keep their final (baseline) width — a scaleX baseline must never
     trap them hidden for reduced-motion users. */
  .pricing-compare-fill {
    transform: none;
  }
}

/* ============================================================
   13. Hero Photo (keduka-01)
   ============================================================ */
.hero-photo {
  position: relative;
  display: block;
  width: 100%;
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow:
    var(--shadow-card-hover),
    0 0 60px -10px rgba(155, 109, 255, .25);
  border: 1px solid var(--border-light);
  background: var(--media-surface, #0b0d12);
}

.hero-photo-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-card);
}

/* Column balance on wide screens. MEASURED at 1899px: the text column ran
   667px tall while the 16:9 photo was only 330px, leaving a 168px
   L-shaped void beside the CTA row — the hero looked half-finished on a
   large monitor.

   Only from 1200px up, and only as a MINIMUM height with object-fit
   cover: below that the photo keeps its intrinsic aspect (no cropping on
   the widths where the columns already balance), and a min-height can
   only ever grow the photo toward the text column, never letterbox it.
   Capped at 30rem so an unusually tall text column can't stretch the
   image into a distorted banner. */
@media (min-width: 1200px) {
  .hero-photo,
  .hero-photo-img {
    height: 100%;
  }

  .hero-photo {
    min-height: min(26rem, 100%);
    max-height: 30rem;
  }

  .hero-photo-img {
    object-fit: cover;
    object-position: center;
  }
}

/* The floating subject cards now layer over the photo */
.hero-visual .hero-card-stack {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.hero-visual .hero-float-card {
  pointer-events: auto;
}

.hero-visual {
  position: relative;
}

/* ============================================================
   14. Showcase ("See KAIS in action")
   ============================================================ */
.showcase-section {
  background: var(--bg-page);
}

.showcase-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.25rem, 3vw, 2rem);
  margin-top: clamp(2rem, 4vw, 3rem);
}

@media (min-width: 900px) {
  .showcase-grid {
    grid-template-columns: 1fr 1fr;
  }
}

.showcase-tile {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.3s ease;
}

.showcase-tile:hover {
  transform: translateY(-6px) scale(1.01);
  box-shadow: var(--shadow-card-hover), var(--glow-blue), var(--inner-top-light);
}

/* Per-product accent on hover: dokai is the orange product. */
.showcase-tile[data-product="dokai"]:hover {
  box-shadow: var(--shadow-card-hover), var(--glow-orange), var(--inner-top-light);
}

.showcase-tile[data-product="kais-learn"]:hover {
  box-shadow: var(--shadow-card-hover), var(--glow-purple), var(--inner-top-light);
}

.showcase-photo {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-bottom: 1px solid var(--border-light);
}

/* Scrim so the screenshot hands off into the card body instead of
   ending on a hard horizontal edge. Decorative and non-interactive —
   it covers the full image area inside a linked card. */
.showcase-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
              rgba(11, 16, 32, .12) 0%,
              transparent 35%,
              rgba(18, 25, 57, .55) 100%);
}

.showcase-photo-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s ease;
}

.showcase-tile:hover .showcase-photo-img {
  transform: scale(1.03);
}

.showcase-copy {
  padding: 1.5rem clamp(1.25rem, 3vw, 2rem);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.showcase-eyebrow {
  align-self: flex-start;
  padding: 0.25rem 0.6rem;
  border-radius: var(--radius-pill);
  background: rgba(106, 168, 255, .12);
  box-shadow: inset 0 0 0 1px rgba(106, 168, 255, .28);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--accent-ink, var(--brand));
}

.showcase-tile[data-product="dokai"] .showcase-eyebrow {
  background: rgba(255, 150, 0, .12);
  box-shadow: inset 0 0 0 1px rgba(255, 150, 0, .3);
}

.showcase-tile[data-product="kais-learn"] .showcase-eyebrow {
  color: var(--accent-ink, var(--brand));
}

.showcase-tile[data-product="dokai"] .showcase-eyebrow {
  color: var(--warn-ink, var(--kais-orange));
}

.showcase-tile-title {
  font-size: clamp(1.15rem, 2vw, 1.45rem);
  font-weight: 800;
  color: var(--text-heading);
  margin: 0;
  line-height: 1.25;
}

.showcase-tile-desc {
  font-size: 0.98rem;
  color: var(--text-body);
  line-height: 1.6;
  margin: 0;
}

.showcase-tile-link {
  align-self: flex-start;
  margin-top: 0.5rem;
  color: var(--accent-ink, var(--brand));
  font-weight: 700;
  text-decoration: none;
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  transition: color 0.2s ease, transform 0.2s ease;
}

.showcase-tile-link:hover {
  color: var(--accent-2-ink, var(--brand-2));
  transform: translateX(2px);
}

.showcase-tile-link:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ============================================================
   15. Final-CTA Backdrop (keduka-02)
   ============================================================ */
.final-cta-backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  overflow: hidden;
}

.final-cta-backdrop img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.28;
  filter: saturate(115%);
}

.final-cta-section::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--cta-scrim,
    linear-gradient(180deg, rgba(11, 16, 32, .55) 0%, rgba(11, 16, 32, .85) 100%));
  z-index: 0;
}

@media (prefers-reduced-motion: reduce) {
  .showcase-tile,
  .showcase-photo-img,
  .showcase-tile-link {
    transition: none;
  }
  .showcase-tile:hover,
  .showcase-tile:hover .showcase-photo-img,
  .showcase-tile-link:hover {
    transform: none;
  }
}
