/* KarateShowCase marketing homepage — v20.
 *
 * WHAT CHANGED FROM v19, AND WHY. v19 was honest and fast but showed the buyer
 * no product above the fold: 974px of type, an address box, and a quiet demo
 * card, with the first screenshot 1,696px down. It also carried thirteen
 * sections in one rhythm — headline block beside a drawn miniature — so a page
 * that had grown to cover the whole platform read as a long uniform scroll.
 *
 * v20 keeps everything that was working (the token layer, the reveal and
 * sticky contracts, the address-entry lane, the SEO head, GA4) and changes
 * three things:
 *
 *   1. THE PROOF IS THE PRODUCT. Where v19 drew a miniature and labelled it
 *      "Illustration", v20 ships a real screenshot of the demo tenant. That
 *      rule — docs/HOMEPAGE-CLAIMS.md, "Drawn, not photographed" — exists for
 *      PHOTOGRAPHS the repository has no rights to. It was never about the
 *      application, and reading it that way cost the page its best evidence.
 *   2. THE HEADER CAN NAVIGATE. v19's nav was brand + Shop + Sign In, so the
 *      only way through the page was to scroll all of it.
 *   3. NINE SECTIONS, NOT THIRTEEN, with the rhythm deliberately varied —
 *      full-bleed, split, and stacked — so no two adjacent sections share a
 *      shape.
 *
 * THIS FILE OWNS COMPOSITION ONLY. Every colour, radius, shadow, type step and
 * duration comes from ks-tokens.v1.css. A raw hex here would be a bug: it
 * could not be themed and it would fork the design system, which is the one
 * thing the token file's own header forbids.
 */

/* ══ SHELL & PRIMITIVES ══════════════════════════════════════════════════ */

/* border-box, and it is not boilerplate here. The mobile sheet is
   `width: min(380px, 100%)` with 2rem of side padding; under content-box that
   is a 444px element, which is exactly the 444px of horizontal overflow this
   page reported and exactly how far off the left edge the open sheet's labels
   were cut. Every fixed-width box on this page assumes the padding is inside
   the width. */
*, *::before, *::after { box-sizing: border-box; }

/* A safety net for ORDINARY overflow — a long unbroken string, a wide table.
   `clip` rather than `hidden` so this never becomes a scroll container and
   breaks `position: sticky` on the header.

   IT DOES NOT CLIP THE MOBILE SHEET, and that is worth stating because two
   attempts here assumed it would. A position:fixed box is laid out against the
   viewport, so no overflow on an ancestor — root included — can clip it; it
   still counts toward the document's scrollable width. The sheet is therefore
   fixed at the source instead: see .v20-menu, which never leaves the viewport.

   Unscoped `html` is safe: this stylesheet is linked by dist/index.html alone,
   which tests/repo/every-stylesheet-is-reachable.spec.ts pins. */
html { overflow-x: clip; }

.v20 {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--ks-text);
  font-size: var(--ks-t-body);
  line-height: 1.6;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

.v20-shell {
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: clamp(1rem, 4vw, 3rem);
}

.v20-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

.v20-skip {
  position: absolute; left: -9999px; top: 0; z-index: var(--ks-z-toast);
  background: var(--ink); color: var(--ink-inverse);
  padding: var(--ks-s-3) var(--ks-s-4); font-weight: 700;
}
.v20-skip:focus { left: 0; }

.v20 a:focus-visible,
.v20 button:focus-visible,
.v20 input:focus-visible {
  outline: 3px solid var(--ks-blue);
  outline-offset: 3px;
  border-radius: var(--ks-r-control-sm);
}

/* Anchored sections must clear the sticky header, or a jump lands with the
   heading tucked underneath it. */
.v20 [id] { scroll-margin-top: 6.5rem; }

.v20 h1, .v20 h2, .v20 h3 {
  font-family: var(--ks-display);
  line-height: 1.08;
  margin: 0;
  text-wrap: balance;
}

/* ══ BUTTONS ═════════════════════════════════════════════════════════════ */

.v20-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--ks-s-2);
  padding: .78rem 1.35rem;
  border-radius: var(--ks-r-control);
  border: 1px solid transparent;
  font: inherit; font-weight: 700; font-size: var(--ks-t-sm);
  text-decoration: none; cursor: pointer;
  white-space: nowrap;
  transition: transform var(--ks-fast) var(--ks-ease),
              box-shadow var(--ks-fast) var(--ks-ease),
              background-color var(--ks-fast) var(--ks-ease);
}
/* Red is the primary ask, per the token file's reservation of it. */
.v20-btn-primary {
  background: var(--primary); color: var(--ks-white);
  box-shadow: var(--ks-e1), var(--ks-sheen);
}
.v20-btn-primary:hover { background: var(--primary-deep); transform: translateY(-1px); box-shadow: var(--ks-e2); }
.v20-btn-ghost {
  background: var(--surface-raised); color: var(--ink);
  border-color: var(--line-strong); box-shadow: var(--ks-e1);
}
.v20-btn-ghost:hover { border-color: var(--ink-3); transform: translateY(-1px); }
.v20-btn-onDark {
  background: var(--ks-white); color: var(--ks-navy); box-shadow: var(--ks-e2);
}
.v20-btn-onDark:hover { transform: translateY(-1px); }
.v20-btn-onDarkGhost {
  background: transparent; color: var(--ks-white);
  border-color: rgba(255, 255, 255, .38);
}
.v20-btn-onDarkGhost:hover { border-color: var(--ks-white); background: rgba(255, 255, 255, .08); }
.v20-btn-lg { padding: .95rem 1.7rem; font-size: 1rem; }

/* ══ HEADER ══════════════════════════════════════════════════════════════ */

.v20-nav {
  position: sticky; top: 0; z-index: var(--ks-z-sticky);
  border-bottom: 1px solid transparent;
  transition: border-color var(--ks-med) var(--ks-ease),
              box-shadow var(--ks-med) var(--ks-ease),
              padding var(--ks-med) var(--ks-ease);
}
/* THE FROSTED PANE IS A PSEUDO-ELEMENT, AND THAT IS LOAD-BEARING.
   backdrop-filter on the <nav> itself establishes a containing block for
   position:fixed DESCENDANTS — so the mobile sheet, which lives inside this
   header, stopped being positioned against the viewport and started being
   positioned against the header's own box. It opened as a 380px panel pinned
   inside a 60px-tall bar with its content running off the left edge; the
   keyboard and focus behaviour all passed while the thing was visually
   unusable, which is why the screenshot caught it and the a11y assertions
   did not. A pseudo-element cannot contain other elements, so the blur lives
   here and the sheet is free. */
.v20-nav::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in oklab, var(--surface) 88%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
}
/* A border only once there is content behind it — ks-sticky-header.v1.js adds
   the class; this file decides what "stuck" looks like. */
.v20-nav.is-stuck {
  border-bottom-color: var(--line);
  box-shadow: 0 1px 18px -8px rgba(12, 23, 48, .28);
}
.v20-nav-in {
  display: flex; align-items: center; gap: var(--ks-s-5);
  padding-block: .7rem;
  transition: padding var(--ks-med) var(--ks-ease);
}
.v20-nav.is-stuck .v20-nav-in { padding-block: .45rem; }

.v20-brand { display: flex; align-items: center; text-decoration: none; flex: 0 0 auto; }
.v20-brand img {
  display: block; width: 142px; height: auto;
  transition: width var(--ks-med) var(--ks-ease);
}
.v20-nav.is-stuck .v20-brand img { width: 124px; }

.v20-menu { display: flex; align-items: center; gap: var(--ks-s-5); margin-left: auto; }
.v20-links { display: flex; align-items: center; gap: var(--ks-s-5); margin: 0; padding: 0; list-style: none; }
.v20-links a {
  font-size: var(--ks-t-sm); font-weight: 600; color: var(--ink-2);
  text-decoration: none; white-space: nowrap;
  padding-block: .3rem;
  border-bottom: 2px solid transparent;
  transition: color var(--ks-fast) var(--ks-ease), border-color var(--ks-fast) var(--ks-ease);
}
.v20-links a:hover { color: var(--ink); border-bottom-color: var(--primary); }

.v20-nav-actions { display: flex; align-items: center; gap: var(--ks-s-4); }
.v20-nav-go {
  font-size: var(--ks-t-sm); font-weight: 600; color: var(--ink-2);
  text-decoration: none; white-space: nowrap;
}
.v20-nav-go:hover { color: var(--ink); }
/* The existing Sign In control keeps its own component class so the root-entry
   script continues to find it; this only sizes it for the new bar. */
.v20-nav .ks-signin-btn {
  padding: .5rem 1.05rem; font-size: var(--ks-t-sm); font-weight: 700;
  border-radius: var(--ks-r-control); border: 1px solid var(--line-strong);
  background: var(--surface-raised); color: var(--ink); cursor: pointer;
  white-space: nowrap;
}
.v20-nav .ks-signin-btn:hover { border-color: var(--ink-3); }

/* ── the hamburger, and the sheet it opens ─────────────────────────────── */
.v20-burger {
  display: none; margin-left: auto;
  width: 44px; height: 44px;               /* a real touch target, not an icon */
  align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--line-strong);
  border-radius: var(--ks-r-control); cursor: pointer; color: var(--ink);
}
.v20-burger span {
  display: block; position: relative; width: 18px; height: 2px;
  background: currentColor; border-radius: 2px;
  transition: transform var(--ks-fast) var(--ks-ease), background-color var(--ks-fast);
}
.v20-burger span::before,
.v20-burger span::after {
  content: ""; position: absolute; left: 0; width: 18px; height: 2px;
  background: currentColor; border-radius: 2px;
  transition: transform var(--ks-fast) var(--ks-ease), top var(--ks-fast) var(--ks-ease);
}
.v20-burger span::before { top: -6px; }
.v20-burger span::after { top: 6px; }
.v20-burger[aria-expanded="true"] span { background: transparent; }
.v20-burger[aria-expanded="true"] span::before { top: 0; transform: rotate(45deg); }
.v20-burger[aria-expanded="true"] span::after { top: 0; transform: rotate(-45deg); }

/* THE BREAKPOINT IS MEASURED, NOT CHOSEN. At 1000px the three links, the three
   actions and the 142px lockup total more than the shell's inner width and the
   row starts shrinking labels. Below it the sheet takes over. */
@media (max-width: 1000px) {
  /* The toggle sits ABOVE the open sheet, so the X it turns into stays visible
     and tappable on top of the panel. Without this the sheet covers its own
     close control and the only ways out are the scrim or the Escape key —
     neither of which a phone user can see. */
  .v20-burger { display: inline-flex; position: relative; z-index: calc(var(--ks-z-modal) + 1); }
  .v20-menu {
    position: fixed; inset: 0 0 0 auto;
    width: min(380px, 100%);
    background: var(--surface);
    box-shadow: var(--ks-e4);
    flex-direction: column; align-items: stretch; justify-content: flex-start;
    gap: 0; margin: 0;
    padding: calc(var(--ks-s-7) + 1.2rem) var(--ks-s-6) var(--ks-s-6);
    /* THE CLOSED SHEET STAYS INSIDE THE VIEWPORT. The obvious drawer — park it
       at translateX(100%) and slide it in — puts a 380px fixed box past the
       right edge, and a fixed box is laid out against the viewport where no
       ancestor's overflow can clip it. Measured: 834px of viewport reporting
       1278px of scrollWidth, and window.scrollTo(500,0) really moved the page
       444px on both tablet and phone. The user could swipe sideways into
       nothing.

       So it fades and rises 14px in place instead of sliding in from off-page.
       Nothing ever sits outside the viewport, the overflow cannot come back,
       and the shorter movement reads calmer than a full drawer throw.

       visibility carries the rest: a panel hidden by opacity alone is still in
       the accessibility tree and still tabbable, so a keyboard user could tab
       into a menu they cannot see. The delay holds it visible for exactly the
       length of the close animation. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-14px);
    transition: opacity var(--ks-med) var(--ks-ease-out),
                transform var(--ks-med) var(--ks-ease-out),
                visibility 0s linear var(--ks-med);
    overflow-y: auto;
    z-index: var(--ks-z-modal);
  }
  .v20-menu[data-open] {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity var(--ks-med) var(--ks-ease-out),
                transform var(--ks-med) var(--ks-ease-out),
                visibility 0s;
  }
  .v20-links { flex-direction: column; align-items: stretch; gap: 0; }
  .v20-links a {
    display: block; padding: var(--ks-s-4) 0; font-size: 1.1rem; font-weight: 700;
    color: var(--ink); border-bottom: 1px solid var(--line);
  }
  .v20-links a:hover { border-bottom-color: var(--line); }
  .v20-nav-actions {
    flex-direction: column; align-items: stretch; gap: var(--ks-s-3);
    margin-top: var(--ks-s-6);
  }
  .v20-nav-actions .v20-btn,
  .v20-nav .ks-signin-btn {
    width: 100%; padding-block: .9rem; font-size: 1rem;
  }
  .v20-nav-go {
    order: -1; text-align: center; padding: .9rem;
    border: 1px solid var(--line-strong); border-radius: var(--ks-r-control);
  }
  .v20-scrim {
    position: fixed; inset: 0; background: rgba(12, 23, 48, .5);
    opacity: 0; pointer-events: none;
    transition: opacity var(--ks-med) var(--ks-ease);
    z-index: var(--ks-z-overlay);
  }
  .v20-scrim[data-open] { opacity: 1; pointer-events: auto; }
}

/* ══ HERO ════════════════════════════════════════════════════════════════ */

.v20-hero {
  position: relative; overflow: hidden;
  padding-block: clamp(2.6rem, 5vw, 4.4rem) clamp(3rem, 6vw, 5rem);
  background:
    radial-gradient(1100px 620px at 82% -8%, color-mix(in oklab, var(--ks-blue) 11%, transparent), transparent 62%),
    radial-gradient(760px 460px at 6% 4%, color-mix(in oklab, var(--ks-red) 7%, transparent), transparent 60%),
    linear-gradient(180deg, var(--surface) 0%, var(--surface-sunken) 100%);
}
.v20-hero-in {
  display: grid; align-items: center;
  grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr);
  gap: clamp(2rem, 5vw, 4.5rem);
}
.v20-eyebrow {
  margin: 0 0 var(--ks-s-4);
  font-family: var(--ks-display);
  font-size: var(--ks-t-label); font-weight: 800;
  letter-spacing: var(--ks-track-label); text-transform: uppercase;
  color: var(--accent-on-surface);
  display: flex; align-items: center; gap: var(--ks-s-3);
}
.v20-eyebrow::before {
  content: ""; width: 30px; height: 3px; border-radius: 2px;
  background: linear-gradient(90deg, var(--ks-red), var(--ks-blue));
}
.v20-h1 {
  font-size: var(--ks-t-hero); font-weight: 800;
  letter-spacing: var(--ks-track-hero);
  margin-bottom: var(--ks-s-5);
}
.v20-h1 span { color: var(--primary); display: block; }
.v20-lede {
  margin: 0 0 var(--ks-s-6);
  font-size: var(--ks-t-lead); color: var(--ink-2); max-width: 34em;
}
.v20-hero-cta { display: flex; flex-wrap: wrap; gap: var(--ks-s-3); margin-bottom: var(--ks-s-6); }

/* The existing-user lane. Deliberately a separate, quieter block: a family
   looking for their dojo must never have to read a sales page to find the
   door, and a prospect must never mistake the address box for a signup. */
.v20-entry {
  padding-top: var(--ks-s-5);
  border-top: 1px solid var(--line);
}
.v20-entry-head {
  display: block; margin-bottom: var(--ks-s-3);
  font-size: var(--ks-t-xs); font-weight: 700; color: var(--ink-3);
  letter-spacing: var(--ks-track-label); text-transform: uppercase;
}
.v20-under { margin: var(--ks-s-3) 0 0; font-size: var(--ks-t-sm); color: var(--ink-3); }

/* THE ADDRESS FIELD ITSELF. Carried over from v19 rather than reinvented: the
   two contrast notes below are measured decisions, and retiring the v19
   stylesheet without bringing these across left the field rendering as a bare
   browser input in the middle of a finished hero. */
.v20-entry { max-width: 32rem; }
.v20-entry .ks-dojo-entry-label {
  display: block; margin-bottom: .6rem;
  font-size: .74rem; font-weight: 800;
  letter-spacing: var(--ks-track-label); text-transform: uppercase;
  color: var(--ink-3);
}
.v20-entry .ks-dojo-entry-form {
  display: flex; align-items: stretch;
  background: var(--surface-raised);
  border-radius: var(--ks-r-lg); overflow: hidden;
  box-shadow: var(--ks-e3), var(--ks-sheen), 0 0 0 1px rgba(12, 23, 48, .07);
  transition: box-shadow var(--ks-med) var(--ks-ease), transform var(--ks-med) var(--ks-ease);
}
.v20-entry .ks-dojo-entry-form:focus-within {
  box-shadow: var(--ks-e3), var(--ks-sheen), 0 0 0 2px var(--ks-blue), 0 0 0 6px rgba(21, 82, 216, .16);
  transform: translateY(-1px);
}
/* --ink-3, not --ink-4: this is the address the reader completes, not a hint.
   --ink-4 on white measures 2.88:1, under the 4.5:1 this size needs. */
.v20-entry .ks-dojo-entry-prefix {
  display: flex; align-items: center; padding-left: 1.15rem;
  font-size: 1rem; font-weight: 500; color: var(--ink-3); white-space: nowrap;
}
.v20-entry .ks-dojo-entry-input {
  flex: 1; min-width: 0; border: 0; outline: 0; background: none;
  font: inherit; font-weight: 700; font-size: 1.05rem; color: var(--ink);
  padding: 1.15rem .4rem;
}
.v20-entry .ks-dojo-entry-input::placeholder { color: var(--ks-n-300); font-weight: 400; }
.v20-entry .ks-dojo-entry-go {
  border: 0; cursor: pointer; font: inherit; font-weight: 700; font-size: 1rem;
  color: var(--ks-white); padding-inline: 1.9rem; letter-spacing: -.005em;
  /* Starts at --ks-red, not --ks-red-bright. White on #FF2449 is 3.75:1, and a
     gradient is only as readable as its lightest stop — the top of the glyphs
     sat on exactly that. #E11133 clears at 4.85:1 and the fall to
     --ks-red-deep still gives the button its depth. */
  background: linear-gradient(180deg, var(--ks-red), var(--ks-red-deep));
}
.v20-entry .ks-dojo-entry-go:hover { filter: brightness(1.07); }
.v20-entry .ks-dojo-entry-go:active { filter: brightness(.94); }
.v20-entry .ks-dojo-entry-status {
  display: block; min-height: 1.4rem; margin-top: .6rem;
  font-size: .9rem; font-weight: 600; color: var(--ks-red-deep);
}
.v20-entry.is-invalid .ks-dojo-entry-form {
  box-shadow: var(--ks-e2), 0 0 0 2px var(--ks-red), 0 0 0 6px rgba(225, 17, 51, .14);
}
/* Below 520px the full "karateshowcase.com/" prefix leaves the input too narrow
   to show a slug being typed, so the prefix drops to its slash. */
@media (max-width: 520px) {
  .v20-entry .ks-dojo-entry-prefix { padding-left: 1rem; font-size: .95rem; }
  .v20-entry .ks-dojo-entry-prefix .long { display: none; }
  .v20-entry .ks-dojo-entry-input { font-size: 1rem; padding-inline: .5rem .3rem; }
  .v20-entry .ks-dojo-entry-go { padding-inline: 1.25rem; }
}

/* ── hero art: one framed surface, one card overlapping it ─────────────── */
.v20-art { position: relative; }
/* The overlapping card is positioned against THIS, not against the column, so
   it cannot land on the demo card that follows. The bottom margin is the room
   the card's overhang needs. */
.v20-art-stage { position: relative; margin-bottom: 4.4rem; }
.v20-frame {
  margin: 0;
  border-radius: var(--ks-r-lg);
  background: var(--surface-raised);
  box-shadow: var(--ks-e4);
  overflow: hidden;
}
.v20-chrome {
  display: flex; align-items: center; gap: var(--ks-s-3);
  padding: .6rem .85rem;
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--line);
}
.v20-dots { display: flex; gap: 6px; flex: 0 0 auto; }
.v20-dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--line-strong); }
.v20-addr {
  flex: 1; min-width: 0;
  padding: .28rem .7rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--ks-r-pill);
  font-size: var(--ks-t-xs); color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.v20-frame img { display: block; width: 100%; height: auto; }

.v20-float {
  position: absolute; left: -6%; bottom: -3.4rem;
  width: min(46%, 274px); margin: 0;
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-e4);
  overflow: hidden;
  background: var(--surface-raised);
}
.v20-float img { display: block; width: 100%; height: auto; }

.v20-art-note {
  margin: var(--ks-s-5) 0 0; text-align: right;
  font-size: var(--ks-t-xs); color: var(--ink-3);
}

/* ══ SECTION RHYTHM ══════════════════════════════════════════════════════ */

.v20-s { padding-block: var(--ks-s-section); position: relative; }
/* A band that orients rather than argues — the audience strip. Full section
   padding there would read as a section; this reads as a threshold. */
.v20-s.thin { padding-block: clamp(1.6rem, 3vw, 2.6rem); }
.v20-s.sunken { background: var(--surface-sunken); border-block: 1px solid var(--line); }
.v20-s.inverse { background: var(--surface-inverse); color: var(--ink-inverse); }

.v20-kicker {
  margin: 0 0 var(--ks-s-3);
  font-size: var(--ks-t-label); font-weight: 800;
  letter-spacing: var(--ks-track-label); text-transform: uppercase;
  color: var(--accent-on-surface);
}
.inverse .v20-kicker { color: var(--accent-on-inverse); }
.v20-h2 {
  font-size: var(--ks-t-h2); font-weight: 800;
  letter-spacing: var(--ks-track-h2);
  margin-bottom: var(--ks-s-4);
}
.v20-sub { margin: 0; font-size: var(--ks-t-lead); color: var(--ink-2); max-width: 42em; }
.inverse .v20-sub { color: rgba(255, 255, 255, .88); font-weight: 500; }

/* Section head used by the stacked variants. */
.v20-head { max-width: 44rem; margin-bottom: var(--ks-s-7); }
.v20-head.center { margin-inline: auto; text-align: center; }

/* Split: copy one side, product the other. `flip` swaps them, and the source
   order stays copy-first so the reading order is right without CSS. */
.v20-split {
  display: grid; align-items: center;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.12fr);
  gap: clamp(2rem, 5vw, 4.5rem);
}
.v20-split.flip > *:first-child { order: 2; }
.v20-split.flip > *:last-child { order: 1; }

.v20-points { margin: var(--ks-s-5) 0 0; padding: 0; list-style: none; display: grid; gap: var(--ks-s-4); }
.v20-points li { position: relative; padding-left: 1.6rem; color: var(--ink-2); }
.v20-points li::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 10px; height: 3px; border-radius: 2px;
  background: linear-gradient(90deg, var(--ks-red), var(--ks-blue));
}
.v20-points b { color: var(--ink); }
.inverse .v20-points li { color: rgba(255, 255, 255, .88); }
.inverse .v20-points b { color: var(--ks-white); }

/* ── the shot: a product screenshot presented as an object ─────────────── */
.v20-shot { margin: 0; position: relative; }
.v20-shot img {
  display: block; width: 100%; height: auto;
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-e3);
  background: var(--surface-raised);
}
.v20-shot figcaption {
  margin-top: var(--ks-s-3);
  font-size: var(--ks-t-xs); color: var(--ink-3);
}
.inverse .v20-shot figcaption { color: rgba(255, 255, 255, .7); }
/* The one action link a band carries — on the Sensei flagship it walks the
   reader into the live demo. Accent-on-inverse is the token derived to stay
   readable on the navy ground. */
.v20-try { margin: var(--ks-s-5) 0 0; }
.v20-try a {
  font-weight: 700; text-decoration: none;
  color: var(--accent-on-inverse);
  border-bottom: 2px solid color-mix(in oklab, var(--accent-on-inverse) 45%, transparent);
  padding-bottom: .15rem;
}
.v20-try a:hover { border-bottom-color: var(--accent-on-inverse); }
/* A second shot tucked under the first, for the sections that carry two. */
.v20-shot-stack { display: grid; gap: var(--ks-s-4); }
/* A tall, narrow companion shot — the Working-toward card beside the wide
   dashboard crop — shown at its natural card width rather than stretched to
   the column, which blew its text up to poster size. */
.v20-shot-narrow { justify-self: center; width: min(100%, 330px); }

/* ══ TILES ═══════════════════════════════════════════════════════════════ */

.v20-tiles {
  display: grid; gap: var(--ks-s-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
}
.v20-tile {
  padding: var(--ks-s-5);
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--ks-r-lg);
  box-shadow: var(--ks-e1);
  transition: transform var(--ks-med) var(--ks-ease), box-shadow var(--ks-med) var(--ks-ease);
}
.v20-tile:hover { transform: translateY(-2px); box-shadow: var(--ks-e2); }
.v20-tile h3 { font-size: var(--ks-t-h3); font-weight: 700; margin-bottom: var(--ks-s-2); }
.v20-tile p { margin: 0; font-size: var(--ks-t-sm); color: var(--ink-2); }
/* A tile that is a link: same object, plus an explicit destination line so the
   affordance never depends on the hover lift alone. */
.v20-tile-link { display: block; text-decoration: none; color: inherit; }
.v20-tile-link .v20-tile-go {
  display: inline-block; margin-top: var(--ks-s-3);
  font-size: var(--ks-t-sm); font-weight: 700; color: var(--accent-on-surface);
}
.v20-tile-link:hover .v20-tile-go { text-decoration: underline; }
.inverse .v20-tile {
  background: rgba(255, 255, 255, .05);
  border-color: rgba(255, 255, 255, .12);
  box-shadow: none;
}
.inverse .v20-tile p { color: rgba(255, 255, 255, .82); }

/* ══ AI SENSEI ═══════════════════════════════════════════════════════════ */

/* THE CONVERSATION GETS THE COLUMN, the character gets a corner.
   Standing them side by side split a ~560px column into a 190px figure and a
   370px screenshot, and at 370px the chat's text was an illegible grey blur —
   the one thing this section exists to let a visitor read. So the screenshot
   takes the full width and the Sensei overlaps its lower-right corner, which
   also reads better: he belongs to the product, rather than presenting it. */
.v20-sensei-art { position: relative; }
.v20-sensei-chat { margin: 0; }
/* HE STANDS UNDER THE CARD, NOT ON IT. Overlapping the corner looked right in
   the abstract and was wrong in fact: the conversation's bubbles run the full
   width of the card to its bottom edge, so every overlap that read as
   "layered" actually sat on the sentence where the Sensei hands the visitor to
   a person — the one line in the section that carries the honesty claim. There
   is no corner here that is not text. */
.v20-sensei-fig {
  width: min(30%, 168px);
  margin: -0.75rem 6% 0 auto;
}
.v20-sensei-fig img {
  display: block; width: 100%; height: auto;
  filter: drop-shadow(0 18px 26px rgba(12, 23, 48, .22));
}
.v20-sensei-chat img {
  display: block; width: 100%; height: auto;
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-e3);
  background: var(--surface-raised);
}

/* ══ CLOSE ═══════════════════════════════════════════════════════════════ */

.v20-close { position: relative; overflow: hidden; }
.v20-close-in { position: relative; z-index: 1; text-align: center; max-width: 46rem; margin-inline: auto; }
.v20-close .v20-hero-cta { justify-content: center; }

/* ══ FOOTER ══════════════════════════════════════════════════════════════ */

.v20-foot {
  background: var(--surface-inverse); color: rgba(255, 255, 255, .78);
  padding-block: var(--ks-s-7);
}
.v20-foot-in { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ks-s-5); }
.v20-foot img { width: 128px; height: auto; }
.v20-foot nav { display: flex; flex-wrap: wrap; gap: var(--ks-s-5); margin-left: auto; }
.v20-foot a { color: rgba(255, 255, 255, .8); text-decoration: none; font-size: var(--ks-t-sm); }
.v20-foot a:hover { color: var(--ks-white); }
.v20-foot p { margin: 0; font-size: var(--ks-t-xs); color: rgba(255, 255, 255, .6); }

/* ══ REVEAL ══════════════════════════════════════════════════════════════
   dojo-landing-reveal.v1.js adds .ks-reveal-ready to <html> only once it has
   an observer that will show these again — so with no JS, no observer, or
   reduced motion, everything below simply never hides. */
.ks-reveal-ready .v20-reveal {
  opacity: 0; transform: translateY(14px);
  transition: opacity var(--ks-slow) var(--ks-ease-out), transform var(--ks-slow) var(--ks-ease-out);
}
.ks-reveal-ready .v20-reveal.is-revealed { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .ks-reveal-ready .v20-reveal { opacity: 1; transform: none; }
  .v20-btn:hover, .v20-tile:hover { transform: none; }
}

/* ══ RESPONSIVE ══════════════════════════════════════════════════════════ */

@media (max-width: 980px) {
  /* minmax(0, 1fr), NEVER a bare 1fr. A bare 1fr is minmax(AUTO, 1fr), so the
     column floors at its own min-content and a wide child — here the address
     entry's "karateshowcase.com/" prefix plus its input and button — pushes
     the grid past the viewport. That is the repo's recorded mobile-overflow
     defect class, and it is what put 395px of demo card in a 390px phone. */
  .v20-hero-in { grid-template-columns: minmax(0, 1fr); gap: var(--ks-s-7); }
  .v20-lede { max-width: none; }
  /* The float overlaps by a negative offset, which needs room the single
     column does not have; it becomes a sibling below the frame instead. */
  .v20-art-stage { margin-bottom: var(--ks-s-5); }
  .v20-float { position: static; width: 100%; margin-top: var(--ks-s-4); }
  .v20-art-note { text-align: left; }
  .v20-split { grid-template-columns: minmax(0, 1fr); gap: var(--ks-s-6); }
  .v20-split.flip > *:first-child,
  .v20-split.flip > *:last-child { order: 0; }
}

@media (max-width: 620px) {
  .v20-hero { padding-block: var(--ks-s-6) var(--ks-s-7); }
  .v20-hero-cta .v20-btn { flex: 1 1 100%; }
  .v20-foot-in { flex-direction: column; align-items: flex-start; }
  .v20-foot nav { margin-left: 0; gap: var(--ks-s-4); }
  /* The overlap needs room a phone column does not have, so he steps out from
     under the card and stands beside its caption instead. */
  .v20-sensei-fig { width: min(46%, 172px); margin-inline: auto; }
}
