/* ==========================================================================
   Teachmint X homepage revamp — scoped styles
   All selectors are namespaced under .rv-* so they never collide with the
   existing tm-* / Tailwind layers. Section 1: Hero.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Shared type scale. Every section references these so sizes/weights cannot
   drift apart again:
     --rv-fs-h2     section heading      --rv-fs-lede   section sub-copy
     --rv-fs-title  card / item heading  --rv-fs-body   card / item description
   -------------------------------------------------------------------------- */
:where(.rv-hero, .rv-trust, .rv-eac, .rv-wb, .rv-cc, .rv-vx, .rv-hw, .rv-gc, .rv-tm, .rv-sp) {
  /* 2.5rem = 40px, the agreed desktop size for every section heading. The cap is
     reached at 1111px viewport (3.6vw); narrower screens ramp down to 30.4px and
     phones flatten to 24px in the override below. The hero H1 does NOT use this
     token — it has its own size in .rv-hero__title — so it is unaffected. */
  --rv-fs-h2: clamp(1.9rem, 3.6vw, 2.5rem);
  --rv-fw-h2: 500;
  --rv-fs-lede: clamp(1rem, 1.35vw, 1.15rem);
  --rv-fs-title: clamp(1.15rem, 1.75vw, 1.6rem);
  --rv-fw-title: 600;
  --rv-fs-body: clamp(0.9rem, 1.1vw, 1rem);
  /* one card radius everywhere, and one (smaller) radius for media nested
     inside a card so it reads as contained rather than competing */
  --rv-radius-card: 18px;
  --rv-radius-media: 12px;
}

/* Phones: one flat size for every section heading. Overriding the token hits all
   eight headings at once; the hero H1 is untouched because it has its own size
   (.rv-hero__title), not this token. Must sit after the block above — both use
   :where() so specificity is 0 and source order decides. */
@media (max-width: 640px) {
  :where(.rv-hero, .rv-trust, .rv-eac, .rv-wb, .rv-cc, .rv-vx, .rv-hw, .rv-gc, .rv-tm, .rv-sp) {
    --rv-fs-h2: 24px;
  }
}

/* --------------------------------------------------------------------------
   Text reveal. Headings and sub-copy rise a little and fade in as their
   section comes into view, once each.

   The hidden state lives on .rv-reveal, which is added by JS (initReveals) —
   never in the markup — so if scripts fail or are blocked the copy renders
   normally instead of being stuck at opacity 0. Only the text nodes are
   transformed, never a sticky container (.rv-cc__intro / .rv-cc__card), because
   a transform on those would break the pinning. (.rv-wb__inner was on that list
   while the whiteboard was pinned; it no longer is.)
   -------------------------------------------------------------------------- */
.rv-reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.95s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.95s cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: var(--rv-reveal-delay, 0ms);
}
.rv-reveal.is-revealed {
  opacity: 1;
  transform: none;
}
/* Hero heading: a slow word-by-word fade. The word spans are wrapped by JS
   (initHeroTitleReveal), and the accent span is kept whole — it is the H1's
   line-break control, so splitting it would let the break move.
   Plain inline spans, opacity only — no transform, no inline-block — so line
   breaking and `text-wrap: balance` are completely unaffected. */
.rv-hero__title .rv-word {
  opacity: 0;
  /* Short fade on a tight stagger: consecutive words overlap heavily, so the
     reveal reads as one continuous wave instead of seven discrete words
     appearing in turn. A long fade on a wide stagger looked choppy. */
  transition: opacity 0.75s cubic-bezier(0.33, 1, 0.68, 1);
  transition-delay: var(--rv-word-delay, 0ms);
}
/* Promoted only while animating; initHeroTitleReveal clears it on completion so
   the layers are not held for the life of the page. */
.rv-hero__title.is-words-animating .rv-word {
  will-change: opacity;
}
.rv-hero__title.is-words-revealed .rv-word {
  opacity: 1;
}

/* Belt and braces: if the class is ever applied under reduced motion, the copy
   still shows. initReveals() also declines to add it in the first place. The
   hero selector is listed too so its higher specificity cannot reinstate the
   transition here. */
@media (prefers-reduced-motion: reduce) {
  .rv-reveal,
  .rv-reveal.is-revealed,
  .rv-hero__title .rv-word {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   Header — glass nav (v4.0)

   A translucent pill floating OVER the hero, which is why there is no spacer
   any more: the old headerTMX was an opaque black bar that pushed the page
   down, this one overlaps and the hero reserves the room in its own padding
   (see --rv-nav-space, consumed by .rv-hero and --rv-wb-nav).

   Open state grows the same pill downward into one continuous glass surface.
   The growth is an animated height on .rv-nav__panels set by JS from the active
   panel, not a CSS transition on `auto` (which does not animate) and not
   grid-template-rows 0fr->1fr (too new to rely on here). Panels therefore stay
   in the layout when closed — visibility, never display — so they can be
   measured at any time; visibility:hidden also keeps their links out of the
   tab order, which display:none would do but max-height would not.
   ========================================================================== */
/* Nav geometry lives on :root, not on .rv-nav, because the hero's top padding
   and the whiteboard's sticky offset both have to clear the nav and are NOT its
   descendants — a custom property on .rv-nav would not reach them. One place to
   change the height, three places that stay in step. */
:root {
  --rv-nav-h: 64px; /* the pill itself */
  --rv-nav-inset: 16px; /* its gap to the viewport edges */
  --rv-nav-space: calc(var(--rv-nav-h) + var(--rv-nav-inset));
}
@media (max-width: 1194px) {
  :root {
    --rv-nav-h: 58px;
    --rv-nav-inset: 12px;
  }
}

.rv-nav {
  --rv-nav-ink: #1f3965; /* brand navy — the value behind --tm-color-bl-1 */
  --rv-nav-ink-dark: #162c52;
  /* The dropdown descriptions are 12.5px, so they need 4.5:1. Worst case is
     where the open panel overlaps the hero panel's near-black screen: 88% white
     over black composites to ~#e0e0e0, and the lighter #5b6d8a this started as
     measured only 3.9:1 there. #47597a is 5.3:1 against the same backdrop and
     still clearly secondary to the #1f3965 titles (8.7:1). */
  --rv-nav-muted: #47597a;
  --rv-nav-radius: 18px;

  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Above the page, below the lead-form modal (which is z-index 10000). */
  z-index: 60;
  padding: var(--rv-nav-inset);
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
  /* The element spans the full width including its gutter; without this the
     transparent gutter would swallow clicks aimed at the hero underneath. */
  pointer-events: none;
}
.rv-nav > * {
  pointer-events: auto;
}

.rv-nav__shell {
  /* Glass, as three variables so each state can dial it independently. The
     RESTING state is the lightest — barely frosted, so the classroom wall reads
     through it — while the open and scrolled states below turn it back up,
     because both have to stay legible over content rather than over a flat wall.
     Restating all three in those states is deliberate: inheriting a thinner
     blur into them is what would quietly undo their legibility. */
  /* Against the hero's flat wall the blur barely shows — what reads as "frost"
     is almost entirely the white fill, so that is the number to keep low. At
     0.18 the wall still comes through clearly and the navy label text measures
     7.9:1 against it, so there is contrast headroom left if this needs to go
     lighter again. */
  --rv-nav-glass: rgba(255, 255, 255, 0.18);
  --rv-nav-blur: 10px;
  --rv-nav-sat: 130%;

  position: relative;
  max-width: 1280px;
  margin: 0 auto;
  border-radius: var(--rv-nav-radius);
  background-color: var(--rv-nav-glass);
  -webkit-backdrop-filter: blur(var(--rv-nav-blur)) saturate(var(--rv-nav-sat));
  backdrop-filter: blur(var(--rv-nav-blur)) saturate(var(--rv-nav-sat));
  /* Softened with the fill: a bright white hairline around a nearly clear pill
     reads as a separate outline rather than the edge of the glass. */
  border: 1px solid rgba(255, 255, 255, 0.45);
  box-shadow: 0 10px 34px rgba(20, 40, 77, 0.08);
  /* Clips the growing panel to the rounded corners. */
  overflow: hidden;
  transition: background-color 0.3s ease, box-shadow 0.3s ease,
    -webkit-backdrop-filter 0.3s ease, backdrop-filter 0.3s ease;
}
/* Without blur support the same alpha reads as dirty glass, so go nearly
   opaque instead — legibility over effect. */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .rv-nav__shell {
    background-color: rgba(255, 255, 255, 0.93);
  }
}
/* Past the hero the sections behind are white, and 55% white on white is
   invisible — so the glass firms up once the page has scrolled. Not in the
   design (which only shows the nav over the hero photo), but the nav is fixed
   and has to stay legible over everything below it. */
.rv-nav.is-stuck .rv-nav__shell {
  --rv-nav-glass: rgba(255, 255, 255, 0.82);
  --rv-nav-blur: 20px;
  --rv-nav-sat: 180%;
  border-color: rgba(255, 255, 255, 0.75);
  box-shadow: 0 8px 28px rgba(20, 40, 77, 0.14);
}
/* Open, the shell covers whatever is behind it — on the homepage that is the
   panel's near-black screen, and 55% white over it dropped the dropdown's
   descriptions to roughly 2:1 contrast. Firmer glass while a menu is open, back
   to the lighter fill when it closes. */
.rv-nav.is-open .rv-nav__shell {
  --rv-nav-glass: rgba(255, 255, 255, 0.88);
  --rv-nav-blur: 20px;
  --rv-nav-sat: 180%;
  border-color: rgba(255, 255, 255, 0.75);
  box-shadow: 0 16px 44px rgba(20, 40, 77, 0.16);
}

.rv-nav__bar {
  display: flex;
  align-items: center;
  gap: 18px;
  height: var(--rv-nav-h);
  padding: 0 18px;
}

.rv-nav__logo {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}
.rv-nav__logo img {
  display: block;
  width: 150px;
  height: auto;
}

/* ----- Desktop menu ----- */
.rv-nav__menu {
  flex: 1 1 auto;
  min-width: 0;
}
.rv-nav__list {
  display: flex;
  align-items: center;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.rv-nav__trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 10px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  color: var(--rv-nav-ink);
  white-space: nowrap;
  cursor: pointer;
}
/* The underline the design shows on the open item. */
.rv-nav__trigger::after {
  content: '';
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 1px;
  height: 2px;
  border-radius: 2px;
  background-color: var(--rv-nav-ink);
  opacity: 0;
  transform: scaleX(0.4);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.rv-nav__trigger:hover {
  color: var(--rv-nav-ink-dark);
}
.rv-nav__trigger[aria-expanded='true']::after {
  opacity: 1;
  transform: none;
}
.rv-nav__chev {
  width: 11px;
  height: 8px;
  flex: 0 0 auto;
  transition: transform 0.24s ease;
}
.rv-nav__trigger[aria-expanded='true'] .rv-nav__chev,
.rv-nav__acc-trigger[aria-expanded='true'] .rv-nav__chev {
  transform: rotate(180deg);
}

/* ----- Buttons ----- */
.rv-nav__actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
}
.rv-nav__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 40px;
  padding: 0 18px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.rv-nav__btn--solid {
  border: 1px solid var(--rv-nav-ink);
  background-color: var(--rv-nav-ink);
  color: #ffffff;
}
.rv-nav__btn--solid:hover {
  background-color: var(--rv-nav-ink-dark);
  border-color: var(--rv-nav-ink-dark);
  color: #ffffff;
}
.rv-nav__btn--ghost {
  border: 1px solid var(--rv-nav-ink);
  background-color: rgba(255, 255, 255, 0.9);
  color: var(--rv-nav-ink);
}
.rv-nav__btn--ghost:hover {
  background-color: #ffffff;
  color: var(--rv-nav-ink);
}
.rv-nav__btn--sm {
  height: 36px;
  padding: 0 14px;
}
.rv-nav__btn--full {
  width: 100%;
  height: 48px;
}

/* ----- Dropdown panels ----- */
.rv-nav__mob {
  display: none;
}
.rv-nav__panels {
  position: relative;
  height: 0;
  overflow: hidden;
  transition: height 0.34s cubic-bezier(0.22, 1, 0.36, 1);
}
.rv-nav__panel {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  visibility: hidden;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.24s ease, transform 0.24s ease;
}
.rv-nav__panel.is-active {
  visibility: visible;
  opacity: 1;
  transform: none;
}
.rv-nav__panel-inner {
  /* Shared by the featured banner and the grid that sits under it. */
  --rv-nav-feature-w: 66%;

  padding: 14px 18px 24px;
  border-top: 1px solid rgba(31, 57, 101, 0.1);
}

/* ----- Featured banner + group label (Teachmint X panel) ----- */
.rv-nav__feature {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  /* Two thirds of the panel, as the wireframe shows. The item grid below reads
     the same variable (.rv-nav__grid--narrow) so the banner's right edge and the
     third column line up instead of drifting apart at wide viewports. */
  width: var(--rv-nav-feature-w);
  min-width: 320px;
  padding: 16px 22px;
  border-radius: 14px;
  background-color: rgba(219, 234, 250, 0.75);
  text-decoration: none;
  transition: background-color 0.18s ease;
}
.rv-nav__feature:hover {
  background-color: rgba(205, 226, 248, 0.9);
}
.rv-nav__feature-text {
  display: block;
  min-width: 0;
}
.rv-nav__feature-title {
  display: block;
  font-size: 20px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--rv-nav-ink);
}
.rv-nav__feature-desc {
  display: block;
  margin-top: 3px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--rv-nav-muted);
}
.rv-nav__feature-arrow {
  width: 24px;
  height: 17px;
  flex: 0 0 auto;
  color: var(--rv-nav-ink);
  transition: transform 0.2s ease;
}
.rv-nav__feature:hover .rv-nav__feature-arrow {
  transform: translateX(4px);
}
.rv-nav__group-label {
  margin: 20px 0 6px;
  padding-inline: 12px;
  font-size: 15px;
  font-weight: 600;
  color: var(--rv-nav-ink);
}

.rv-nav__grid {
  display: grid;
  /* Column count is per group, set inline from the macro's data (`cols`): the
     Teachmint X panel runs four across, everything else stays at three. */
  grid-template-columns: repeat(var(--rv-nav-cols, 3), minmax(0, 1fr));
  /* Row gap matches the column gap so the cards sit in an even field. It was 2px,
     which was survivable when the tiles were borderless text but reads as cards
     colliding now that they are outlined — Solutions and Support both showed it.
     The Teachmint X panel overrides both values (--roomy). */
  gap: 14px;
}
/* The featured panel's grid gets real breathing room — at three across with the
   default gaps it read as a crowded block. */
.rv-nav__grid--roomy {
  gap: 16px 24px;
}
/* A featured banner followed STRAIGHT by its items, with no items_label between
   them — the Solutions group. The label was carrying the whole separation
   (.rv-nav__group-label's own 20px top margin plus its 6px bottom), so a group
   without one left the banner and the first row of cards touching at 0px.
   20px matches the label's top margin, so the space below a banner is the same
   number in both cases; a bare 49px — the label's full block — would read as a
   hole rather than a gap.
   Adjacent-sibling on purpose: the Teachmint X panel still has its label, so this
   cannot match there and its 20/6 rhythm is untouched. --media is listed for the
   Devices layout, which has no banner today but would hit the same 0px if one
   were ever added. */
.rv-nav__feature + .rv-nav__grid,
.rv-nav__feature + .rv-nav__media {
  margin-top: 20px;
}
/* Footnote under a panel's items. */
.rv-nav__note {
  margin: 22px 0 0;
  padding-inline: 12px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--rv-nav-muted);
}
.rv-nav__tile {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px;
  /* A hairline in the RESTING state, so each item reads as a card filling its
     grid cell instead of text floating in the panel's whitespace. box-sizing is
     border-box globally here, so this costs no layout — the tiles do not shift.
     Deliberately faint: at full strength four columns of boxed rows turn the
     panel into a table. */
  border: 1px solid rgba(31, 57, 101, 0.1);
  border-radius: 12px;
  text-decoration: none;
  transition: background-color 0.18s ease, border-color 0.18s ease;
}
.rv-nav__tile:hover {
  border-color: rgba(31, 57, 101, 0.18);
  background-color: rgba(31, 57, 101, 0.06);
}
/* Icon slot. The BOX is always 24px so every label sits on the same left edge;
   --rv-icon-s scales only the artwork inside it, correcting for the six icons
   having been drawn at six different artboard sizes (see the icon_scale note in
   the macro). Scaled-up glyphs paint a couple of pixels outside the box, which
   is deliberate — the box is for alignment, not clipping. */
.rv-nav__tile-ico {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex: 0 0 auto;
  margin-top: 2px;
}
.rv-nav__tile-icon {
  width: calc(24px * var(--rv-icon-s, 1));
  height: calc(24px * var(--rv-icon-s, 1));
  /* The global `img { max-width: 100% }` would clamp any icon scaled past its
     24px box straight back to 24px — the third time that rule has bitten this
     stylesheet. */
  max-width: none;
}
.rv-nav__tile-text {
  display: block;
  min-width: 0;
}
.rv-nav__tile-title {
  display: block;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--rv-nav-ink);
}
.rv-nav__tile-desc {
  display: block;
  margin-top: 3px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--rv-nav-muted);
}
/* Icon-and-heading only (no description): centre the label against the icon.
   flex-start is right for a two-line block, where the icon should sit level with
   the title, but on a single line it leaves the label hanging at the icon's top
   edge. MUST stay after .rv-nav__tile — same specificity, so source order is
   what decides, and sitting above it is exactly why an earlier attempt at this
   did nothing. Driven by a class from the template rather than
   :not(:has(…)) — browser support for :has() is not something to lean on here. */
.rv-nav__tile--flat {
  align-items: center;
}
.rv-nav__tile--flat .rv-nav__tile-ico {
  margin-top: 0;
}
/* Support and Partner are bare link lists — no icon, no description. Their
   buttons are deliberately IDENTICAL to the Teachmint X panel's: same 4-column
   grid (cols: 4 in the macro's data), same 16/24 gaps, and the same box, filling
   its cell. Four across is what makes them the right size — three across gave
   each label a box half the panel wide.

   min-height is the MEASURED height of a Teachmint X tile — 50px, set by its
   24px icon box plus the tile's 12px padding and the icon artwork's own
   overshoot. Without an icon these boxes come out shorter, so the two panels'
   buttons would be subtly different sizes side by side. Re-measure this if the
   tile padding or the icon box ever changes.

   DO NOT re-add justify-self: start here. Shrinking each box to hug its label
   was tried and rejected — it read as cropping the text. */
.rv-nav__grid--list {
  gap: 16px 24px;
}
.rv-nav__grid--list .rv-nav__tile {
  min-height: 48px;
}

/* Devices: product shots rather than icons. */
.rv-nav__media {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 18px;
}
.rv-nav__card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 232px;
  padding: 10px;
  border-radius: 14px;
  text-decoration: none;
  transition: background-color 0.18s ease;
}
.rv-nav__card:hover {
  background-color: rgba(31, 57, 101, 0.06);
}
.rv-nav__card-img {
  width: 100%;
  height: auto;
  border-radius: 8px;
}
.rv-nav__card-title {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--rv-nav-ink);
}
.rv-nav__card-desc {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--rv-nav-muted);
}
/* Centred against the product cards rather than sitting on their baseline,
   where it read as stranded at the bottom-left of the panel. align-self beats
   the row's flex-end, so the two cards keep their own bottom alignment. */
.rv-nav__media-cta {
  align-self: center;
}

/* ----- Mobile bar + drawer ----- */
.rv-nav__call {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--rv-nav-ink);
  border-radius: 8px;
  font-size: 13px;
  font-weight: 500;
  color: var(--rv-nav-ink);
  text-decoration: none;
  white-space: nowrap;
}
.rv-nav__burger,
.rv-nav__close {
  /* 44px hit area on a 24px glyph — WCAG 2.5.8 target size. */
  box-sizing: content-box;
  width: 24px;
  height: 24px;
  padding: 10px;
  cursor: pointer;
}
.rv-nav__drawer {
  /* common3.js toggles this to `block` via inline style, as it does on main. */
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow-y: auto;
  background-color: #f7f9fc;
  animation: rv-nav-drawer-in 0.26s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes rv-nav-drawer-in {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
}
.rv-nav__drawer-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 8px 10px 16px;
  background-color: #ffffff;
}
.rv-nav__drawer-body {
  padding-bottom: 32px;
}
.rv-nav__drawer-link,
.rv-nav__acc-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 17px 20px;
  border: 0;
  border-bottom: 1px solid #e6ecf5;
  background: transparent;
  font: inherit;
  font-size: 17px;
  font-weight: 500;
  color: var(--rv-nav-ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.rv-nav__acc-trigger .rv-nav__chev {
  width: 13px;
  height: 9px;
}
.rv-nav__acc-panel {
  padding: 4px 20px 14px;
  border-bottom: 1px solid #e6ecf5;
  animation: rv-nav-acc-in 0.24s ease both;
}
@keyframes rv-nav-acc-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
}
.rv-nav__acc-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 0;
  font-size: 15px;
  font-weight: 500;
  color: var(--rv-nav-ink);
  text-decoration: none;
}
.rv-nav__acc-link .rv-nav__tile-ico {
  margin-top: 0;
}
/* Trailing chevron on every drawer row. On a phone there is no hover to reveal
   that a row is a link, so the arrow is what makes it read as tappable.
   margin-left: auto pins it to the right edge whatever the label's length, and
   flex-shrink: 0 keeps it from being squeezed by a long one
   ("Our Authorized Partners" on a 320px screen). */
.rv-nav__acc-arrow {
  width: 8px;
  height: 14px;
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--rv-nav-ink);
}
.rv-nav__note--acc {
  margin: 10px 0 2px;
  padding-inline: 0;
  font-size: 12.5px;
}
/* The featured card in the drawer. It shares .rv-nav__feature with the desktop
   panel and only overrides what a phone needs: full width instead of two thirds
   of a panel, tighter padding, and a smaller title. No description — the card is
   one line here, as the design shows, and the desc would push the six product
   rows below the fold on a small screen.

   It replaced a plain indented link row (.rv-nav__acc-link--feature), which read
   as a seventh product rather than as the group's own destination. */
.rv-nav__acc-feature {
  width: 100%;
  min-width: 0;
  margin: 8px 0 4px;
  padding: 13px 16px;
}
.rv-nav__acc-feature .rv-nav__feature-title {
  font-size: 16px;
}
.rv-nav__acc-feature .rv-nav__feature-arrow {
  width: 20px;
  height: 14px;
}
/* "What's Inside" over the rows: uppercase and small, so it reads as a divider
   rather than as another tappable row. */
.rv-nav__group-label--acc {
  margin: 14px 0 2px;
  padding-inline: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--rv-nav-muted);
}
.rv-nav__drawer-cta {
  padding: 20px;
}

/* Keyboard focus, everywhere in the nav. */
.rv-nav :focus-visible {
  outline: 2px solid var(--rv-nav-ink);
  outline-offset: 2px;
  border-radius: 8px;
}

@media (prefers-reduced-motion: reduce) {
  .rv-nav__shell,
  .rv-nav__panels,
  .rv-nav__panel,
  .rv-nav__chev,
  .rv-nav__trigger::after {
    transition: none;
  }
  .rv-nav__drawer,
  .rv-nav__acc-panel {
    animation: none;
  }
}

/* Main's nav switches to the burger at 1194px; matched here so the two
   headers behave the same way at the same widths. */
@media (max-width: 1194px) {
  .rv-nav {
    --rv-nav-radius: 16px;
  }
  .rv-nav__menu,
  .rv-nav__actions,
  .rv-nav__panels {
    display: none;
  }
  .rv-nav__mob {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
  }
  .rv-nav__bar {
    gap: 8px;
    padding: 0 8px 0 16px;
  }
  .rv-nav__logo img {
    width: 132px;
  }
}

@media (max-width: 400px) {
  .rv-nav__logo img {
    width: 116px;
  }
  .rv-nav__call {
    padding: 0 8px;
    font-size: 12px;
  }
}

.rv-hero {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  --rv-border: #c9d6ee;

  /* Furniture band, measured off hero-room-v6.webp (2908x2172): the desks begin
     72.79% down the room, so they occupy the bottom 27.21% of its height — and
     its 1.3389 aspect makes that 20.32% of its WIDTH. --rv-room-full is that
     true band at the room's rendered size; each breakpoint restates it for its
     own zoom. Reserving it as padding is what puts the desks BELOW the CTAs
     instead of behind the panel.

     RE-MEASURE THIS WHENEVER THE ROOM PHOTO CHANGES. Method: rasterise the file,
     take the per-row standard deviation of luminance across the middle of the
     image only (x 30..62%, which excludes the window, the door and the book
     cabinet), and take the first row where it stays above 10 — the wall is a
     smooth gradient and sits near 5, the furniture jumps past 70. v5 measured
     74.54% / 19.1vw by the same method, so the two are directly comparable. */
  --rv-room-full: 20.32vw;
  /* ...but only up to a point. Left uncapped the band grows with the viewport:
     at 2000px wide it reserved 382px of desks, which ate the height the panel
     needed and left it at ~18% of the screen instead of the design's 40%. Above
     ~1570px the band holds at 300px and the surplus is cropped off the room's
     bottom instead, so wide screens spend their extra width on the panel. */
  --rv-room-band: min(var(--rv-room-full), 240px);
  /* Clear gap between the CTA row and the first desk. */
  --rv-room-clear: 20px;

  /* The panel, sized by whichever runs out first — a share of the viewport, a
     hard cap, or the FOLD. 1.5649 is the panel asset's own 1241:793 aspect,
     turning available height back into a width; the band is the furniture
     reserve and 350px the rest of the chrome (nav, padding, heading, gaps, CTA
     row).

     The 640px FLOOR inside the height term is the important part. The room is a
     4:3 photo forced to full-bleed, so on a wide, short viewport it is scaled
     hard and cropped — which makes the near desks large. Letting the fold shrink
     the panel to match left it at ~21% of the screen against furniture drawn at
     that scale, and it read as a toy on a wall. The floor holds the panel at the
     design's 40% from ~1366px up; the section then runs modestly past the fold
     and the desk band is what gets clipped, which still leaves the chairs in
     view. Genuinely fixing the proportions on ultra-wide screens needs a room
     photo with a wider field of view, not more CSS. */
  --rv-ifp-w: min(
    40vw,
    820px,
    max(640px, calc((100vh - 350px - var(--rv-room-band)) * 1.5649))
  );

  /* The search bar, measured off hero-ifp.webp (1241x793): the white pill runs
     x 204..1010, y 290..351. Inside it the assistant icon ends at x 264 and the
     mic starts at x 950, so the query text lives between those, not across the
     whole pill. Percentages of the panel, so it stays seated at any size. */
  --rv-ifp-q-left: 22.96%;
  --rv-ifp-q-right: 25.06%;
  /* 36.82%, NOT the pill's own 36.57%. The box is centred on the EduAI BOOK, not
     on the pill: the artwork centres the mark INCLUDING its detached sparkle,
     which sits above and right of the book, so the book itself ends up 2px low.
     Measured inside the pill's rounded outline (a plain rectangle scan reads the
     navy background as ink) — mark x 231..274 y 300..339, of which the book is
     x 231..263 y 306..339, centre 322.5, against the pill's centre of 320.5.
     Since the text's cap centre lands on this box's centre, the box centre has to
     be the BOOK's: 322.5/793 = 40.668%, minus half the height below = 36.82%.
     Aligning to the whole mark instead would move the text the other way and
     leave the book still reading low. */
  --rv-ifp-q-top: 36.82%;
  --rv-ifp-q-height: 7.692%;
  /* Query type tracks the panel's width. Expressed as the min of the three terms
     that can govern that width, so it stays in proportion whichever one is
     binding.
     Trimmed to 1.69% of the panel from the design's 1.934% (and the cap from
     16px to 14px): at 16px the prompt filled ~57% of the search pill's height,
     which read as oversized next to the mic and the assistant icon. 14px puts it
     near 50%, which is where placeholder text normally sits. All three terms were
     scaled by the same 0.875 so the proportions between them are unchanged. */
  --rv-ifp-q-size: min(
    14px,
    calc((100vh - 360px) * 0.0264),
    calc((100vw - 48px) * 0.0169)
  );

  position: relative;
  overflow: hidden;
  text-align: center;
  /* The whole visible furniture band is reserved, plus a gap. Subtracting the
     CTA row's height from it (as a first attempt did) puts the desk line exactly
     that far ABOVE the content's bottom, which sat the CTAs on the desks.
     Top padding clears the floating glass nav, which overlaps this section
     instead of pushing it down — it replaces the old .rv-header-spacer exactly
     (80px nav + 24px gap = the same 104px), so the 350px chrome constant in
     --rv-ifp-w above is still calibrated correctly. */
  padding: calc(var(--rv-nav-space) + 24px) 24px
    calc(var(--rv-room-band) + var(--rv-room-clear));
  /* Safety net only (the room covers the section at every real viewport): the
     room's own top-row average, so any join is invisible. Re-measured for v6. */
  background-color: #b5bdc3;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}

/* ----- Backdrop -----
   Anchored to the bottom so the desks always sit on the section's bottom edge;
   anything that does not fit is cropped off the top. Only `width` changes per
   breakpoint — it has to stay large enough to cover the section's height, which
   is why narrower viewports zoom it. */
.rv-hero__room {
  position: absolute;
  z-index: 0;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  height: auto;
  /* Pushes the room down by however much of its furniture band the cap above
     discarded, so the desks that ARE shown still end at the section's bottom
     edge. A negative bottom margin, not `bottom`, because percentage-and-vw
     margins resolve along the same axis every number here is measured in. */
  margin-bottom: calc(var(--rv-room-band) - var(--rv-room-full));
  /* The global `img { max-width: 100% }` would clamp the zoomed widths below
     back to the section width and leave a bare band above the room. */
  max-width: none;
}

.rv-hero__inner {
  position: relative;
  z-index: 1;
  margin: 0 auto;
  max-width: 1200px;
  /* tm-tfi-max-width carries a 30px bottom padding; dropped so the section's
     own padding is the only thing setting the gap under the CTAs. */
  padding-bottom: 0;
}

/* ----- Headline ----- */
.rv-hero__title {
  margin: 0 auto;
  max-width: none;
  font-weight: 600; /* semi-bold */
  font-style: normal; /* upright — no italics */
  font-size: clamp(2rem, 5.2vw, 3.5rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
  color: var(--rv-navy);
}
.rv-hero__accent {
  /* Block so the headline always breaks after "AI-Powered" — the design puts it
     on two lines at every width. No gradient: the whole H1 is one solid navy,
     inherited from .rv-hero__title. */
  display: block;
}
/* Registered mark in a heading — the hero H1 and the Connected Classroom® App
   h2 both use it, via the regMarked helper in teachmint-v4.0/macros/text.html.
   Not hero-scoped, and sized in em, so any heading that carries a ® gets the
   same treatment at its own size with no extra rule.

   Inter draws this glyph FULL CAP HEIGHT — measured on a canvas at the 56px
   desktop heading, its ink runs 10px to 42px above the baseline, the same 42px
   top as a capital C. That is why it read as another letter in the word rather
   than as a mark. Halved it is 16px tall, and raising it 21px puts its top flush
   with the capitals.

   Both numbers are em, so they track the clamped heading size at every
   breakpoint with no second rule to keep in step. Mind the unit though:
   vertical-align's em resolves against the element's OWN font-size, i.e. the
   halved one — 0.75em here is 0.375em of the heading, the 21px lift. It is not a
   typo for half that.

   line-height: 0 keeps the raised glyph from growing the line box and pushing
   the two headline lines apart. */
.rv-reg {
  font-size: 0.5em;
  vertical-align: 0.75em;
  line-height: 0;
  letter-spacing: 0;
}
/* Holds a word and its mark together as ONE reveal word — see the H1 in
   teachmint-v4.0/macros/hero.html. Inline and unstyled on purpose: it must not
   change how the line breaks. */
.rv-word-keep {
  display: inline;
}

/* ----- Optional subhead (off by default) ----- */
.rv-hero__subhead {
  margin: 16px auto 0;
  max-width: 60ch;
  font-size: clamp(1rem, 1.6vw, 1.15rem);
  line-height: 1.6;
  color: var(--rv-muted);
}

/* ----- The panel ----- */
.rv-hero__device {
  position: relative;
  width: var(--rv-ifp-w);
  margin: 20px auto 0;
}
.rv-hero__ifp {
  display: block;
  width: 100%;
  height: auto;
}

/* ----- The rolling query -----
   A crossfade between absolutely stacked items rather than a vertical roll: a
   roll needs each item to be exactly the window's height, and the window here is
   a percentage of the panel, so there is no em value that would keep the two in
   step as the panel scales. */
.rv-hero__query {
  position: absolute;
  left: var(--rv-ifp-q-left);
  right: var(--rv-ifp-q-right);
  top: var(--rv-ifp-q-top);
  height: var(--rv-ifp-q-height);
  overflow: hidden;
  text-align: left;
  font-size: var(--rv-ifp-q-size);
  line-height: 1.2;
  color: #6b7280; /* placeholder grey, as the design shows */
}
.rv-hero__query-track {
  display: block;
  height: 100%;
}
.rv-hero__query-item {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-30%);
  opacity: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.rv-hero__query-item.is-on {
  opacity: 1;
  transform: translateY(-50%);
}

/* ----- CTAs, under the panel ----- */
/* Layout only — the buttons themselves are the site's shared component
   (tm-* utilities + .hero-cta-w), not styled here. */
.rv-hero__cta {
  flex-wrap: wrap;
  margin-top: 18px;
}

/* ==========================================================================
   Responsive
   ========================================================================== */

/* ----- Large screens: hold, then exceed, the design's share -----
   Above ~1600px the base --rv-ifp-w works AGAINST the panel, for two separate
   reasons, both measured on the rendered page:

     1728x1000 (a 16" MacBook)  642px = 37.1% of the width. The height term is
                                binding, caught by its 640px floor: the 350px
                                chrome constant plus the 240px furniture band
                                leave too little height to reach 40vw.
     2560x1440                  820px = 32.0%. The hard cap is binding. Past
                                ~2050px wide, 40vw exceeds 820px and the cap
                                takes over, so the panel's SHARE of the screen
                                shrinks the wider the display gets, which is
                                backwards.

   Both limits are lifted here, and the share goes to 44vw — a deliberate step
   past the design's 40%, because the panel read small on these screens.

   Width is not the constraint: the room's window ends ~14% in and the door starts
   ~88% across, so a centred 44vw panel spans 28%..72% and keeps ~14% of clear
   wall either side. What actually limits it is how large the panel looks against
   the near desks, which is a judgement rather than a measurement.

   The cap is 1180px because the panel asset is 1241px wide: past that the artwork
   is being upscaled, and a soft panel is worse than a slightly small one. Raising
   it further wants a higher-resolution export first — which is also what an
   ultra-wide display needs, since 44vw there lands well past 1241px (a 3440px
   screen gets 34% rather than 44%).

   The height term is rebuilt, not inherited. The base rule subtracts the
   furniture band as well as 350px of chrome, which double-counts: the band is
   padding the design already allows to be cropped off the bottom, so subtracting
   it shrinks the panel to protect something that is meant to clip. What actually
   has to stay above the fold is the CTA row, and its bottom sits at exactly
   `chrome + panel height` — measured at 1728x1000, 316px + 460px = 776px, which
   matches the rendered value. So the honest limit is (100vh - 316px), and the
   336px here is that plus the 20px of clearance --rv-room-clear stands for.

   The 680px floor keeps a live height term so a genuinely short window shrinks
   the panel instead of pushing the CTAs under the fold. Verified down to
   1728x760 — a 16" with a bookmarks bar and the dock showing. */
@media (min-width: 1600px) {
  .rv-hero {
    --rv-ifp-w: min(
      44vw,
      1180px,
      max(680px, calc((100vh - 336px) * 1.5649))
    );
  }
}

@media (max-width: 1024px) {
  .rv-hero {
    padding-top: calc(var(--rv-nav-space) + 16px);
    padding-inline: 16px;
    /* Room zoomed 1.3x here, so its furniture band is 1.3x wider as a share of
       the section: 20.32vw x 1.3. Leaving it at the desktop value under-reserves
       and the desks creep back up behind the CTAs. */
    --rv-room-full: 26.42vw;
    /* The 360px FLOOR is the same guard the desktop rule carries and this one was
       missing. Without it a short landscape tablet collapses the panel: at
       1024x600 the height term is (600 - 320 - 240) x 1.5649 = 63px, so the device
       rendered 63px wide — 6% of the screen, a sliver on a classroom wall. The
       floor holds it at 360px there (35% of the width) and the section runs past
       the fold instead, which is the same trade the desktop rule makes. Checked at
       1024x600 and 960x600 that the CTAs still land above the fold. */
    --rv-ifp-w: min(
      56vw,
      640px,
      max(360px, calc((100vh - 320px - var(--rv-room-band)) * 1.5649))
    );
  }
  .rv-hero__room {
    width: 130%;
  }
}

@media (max-width: 640px) {
  .rv-hero {
    /* 28px, not 12px. At 12 the nav, the two-line H1 and the panel read as one
       crowded block on a phone — there was ~17px between the nav's bottom edge
       and the cap line. Paired with the 28px under the H1
       (.rv-hero__device below); the 300px constant in --rv-ifp-w absorbs both. */
    padding-top: calc(var(--rv-nav-space) + 28px);
    /* Phones zoom the room 1.5x — at 100% it is shorter than the section once
       the column narrows, which would leave a bare band above it. Both the
       furniture reserve and the panel cap scale by the same 1.5 so the desks
       still land below the CTAs. */
    /* Room zoomed 1.9x: at 1.5x it was shorter than the section and left a bare
       band above it. The reserve tracks the zoom — 20.32vw x 1.9. */
    --rv-room-full: 38.61vw;
    --rv-room-clear: 14px;
    /* 300px, up from 260px: this constant is everything above and below the
       panel that the panel must leave room for, and the two spacing increases
       above added 40px of it (16px of top padding + 24px under the H1). Left at
       260 the panel would keep its old height and push the CTAs down onto the
       desks on a short phone. */
    --rv-ifp-w: min(
      88vw,
      calc((100vh - 300px - var(--rv-room-band)) * 1.5649)
    );
    /* Proportional type would put the prompt at 6px on a 390px phone — correct
       against the panel, unreadable against a thumb. Floored at 9px, which still
       clears the pill's height and its text zone's width. The floor is what
       actually applies at every phone width, so the desktop trim above does not
       shrink this further. */
    --rv-ifp-q-size: max(9px, calc((100vw - 32px) * 0.0169));
  }
  .rv-hero__room {
    width: 190%;
  }
  /* mob-tm-tfi-max-width (new-landing-tmx5.css) sets width: 90%, which left
     322px for a second line needing 323 — one pixel short, so the heading
     wrapped to three lines. Overridden here rather than shrinking the type to
     fit a self-inflicted margin, and scoped to the hero so the shared utility
     is untouched everywhere else that uses it. */
  .rv-hero__inner {
    width: 100%;
    padding-inline: 0;
  }
  .rv-hero__title {
    font-size: clamp(1.2rem, 5.9vw, 1.75rem);
  }
  /* 28px under the headline, up from 14px — the H1 and the panel were almost
     touching. The CTA gap stays at 14px: it sits between the panel and the
     buttons, which the design keeps tight, and widening it would eat into the
     furniture reserve at the bottom of the section. */
  .rv-hero__device {
    margin-top: 28px;
  }
  .rv-hero__cta {
    margin-top: 14px;
  }
}

/* Reduced motion — the query stops cycling (handled in initHeroQuery, which
   leaves the first prompt showing) and its crossfade is neutralised. */
@media (prefers-reduced-motion: reduce) {
  .rv-hero__query-item {
    transition: none;
  }
}

/* ==========================================================================
   Section 2 — Trust strip (auto-scrolling logo marquee)
   ========================================================================== */
.rv-trust {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  --rv-border: #c9d6ee;
  background: #ffffff;
  /* 48px top: the hero used to fade out into a white panel, so 8px was enough
     air under it. It now ends on the photo's hard bottom edge, and the heading
     needs clearance from it. */
  padding: 48px 0 64px;
  overflow: hidden;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.rv-trust__title {
  margin: 0 auto 44px;
  padding: 0 16px;
  text-align: center;
  color: var(--rv-navy);
  /* Was the odd one out in all three properties: its own clamp (max 32px), 600
     weight and a 1.25 line-height. Now identical to every other section heading
     — same token for size and weight, same 1.14 leading — so it reads as one of
     the set on desktop and on phones. */
  font-weight: var(--rv-fw-h2);
  font-style: normal;
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
}
/* Viewport clips the track and fades both edges */
.rv-trust__viewport {
  position: relative;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(
    to right, transparent, #000 8%, #000 92%, transparent
  );
  mask-image: linear-gradient(
    to right, transparent, #000 8%, #000 92%, transparent
  );
}
.rv-trust__track {
  display: flex;
  width: max-content;
  animation: rv-trust-scroll 45s linear infinite;
}
.rv-trust__viewport:hover .rv-trust__track {
  animation-play-state: paused;
}
.rv-trust__item {
  flex: 0 0 auto;
  width: 168px;
  padding: 0 12px;
  text-align: center;
}
.rv-trust__logo {
  width: 76px;
  height: 76px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.rv-trust__logo img,
.rv-trust__logo svg {
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Monochrome, per the approved reference: the crests arrive in a dozen
     different palettes, and desaturating them keeps the strip reading as one
     calm row instead of competing with the sections around it. The captions
     stay navy.

     opacity on top of that, because grayscale alone left the dense crests
     unreadable at 76px — Ateneo de Naga and Colegio San Agustin are near-black
     artwork that collapses into a blob at this size.

     opacity, specifically, and NOT contrast/brightness. Compositing at 0.82 over
     the white section lifts black ink to ~46 while leaving white at 255, and the
     lift scales with how dark a pixel is: the dense crests open up and the pale
     ones (Delhi Public School, VIT) barely move, which is the whole point.

     contrast(0.7) brightness(1.08) was tried first and REJECTED. The crests are
     RGBA, so their PNG headers suggested transparent backgrounds — they are not,
     they are opaque white. Anything that darkens white therefore painted a grey
     rounded square behind every logo. Verify against a render, not the file
     header, before touching this again. */
  filter: grayscale(1) opacity(0.82);
}
.rv-trust__name {
  margin-top: 14px;
  color: var(--rv-navy);
  font-size: 15px;
  font-weight: 400; /* regular — the logo carries the emphasis, not the caption */
  line-height: 1.35;
}
.rv-trust__country {
  margin-top: 2px;
  color: var(--rv-muted);
  font-size: 0.85em;
  line-height: 1.3;
}
/* Track holds two identical copies; shifting by -50% loops seamlessly. */
@keyframes rv-trust-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (max-width: 640px) {
  .rv-trust { padding-top: 32px; padding-bottom: 44px; }
  .rv-trust__item { width: 138px; }
  .rv-trust__logo { width: 62px; height: 62px; }
  .rv-trust__name { font-size: 13px; margin-top: 12px; }
  .rv-trust__country { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
  .rv-trust__track { animation: none; }
  .rv-trust__viewport { overflow-x: auto; } /* allow manual scroll instead */
}

/* ==========================================================================
   Section 3 — EduAI (IFP panel + self-advancing agent list)
   ========================================================================== */
.rv-eac {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  --rv-eac-dim: #9fb0c9; /* idle card text */
  --rv-eac-rule: #1f7ae0; /* live card timer fill */
  --rv-eac-sep: #e4eaf3; /* idle card baseline */
  --rv-eac-max: 1200px;

  /* ONE height drives both columns: the panel gets it via aspect-ratio, the
     list clips to exactly the same number of pixels, so the two line up at
     every viewport without a second magic number to keep in sync.
     100vh - 300px is the space left once the nav, heading and section padding
     have taken their share; the clamp stops it collapsing on a short window or
     ballooning on a tall one. */
  --rv-eac-media-h: clamp(240px, calc(100vh - 300px), 600px);

  background: #ffffff;
  padding: 40px 0 44px;
  /* The panel deliberately bleeds past its column so the clips' zoom is not
     cropped (see .rv-eac__frame). On a phone the left bleed is wider than the
     body's side padding, which would otherwise let the page scroll sideways.
     `clip` rather than `hidden`: it contains the bleed without making this
     section a scroll container. */
  overflow-x: clip;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}

/* Head: left heading, right lede */
.rv-eac__head {
  max-width: var(--rv-eac-max);
  /* Sets the gap between the lede and the top of the panel, so the head reads
     as its own block rather than crowding the row below. The rendered gap runs
     ~14px larger than this number — the lede's last line carries half-leading
     below its text — so 80px here shows as ~94px. */
  margin: 0 auto 80px;
  padding: 0 24px;
  display: flex;
  gap: 40px;
  justify-content: space-between;
  /* The lede sits on the VERTICAL CENTRE of the left column, not its top. The
     left column is three lines tall (the lockup plus two of heading) against the
     lede's three shorter ones, and top-aligning them left the lede reading as
     stuck to the ceiling with a hole beneath it.
     Only right for the ROW layout — the 1024px block puts this head into a
     column, where `center` would shrink-wrap the heading and centre it
     horizontally, so that block restores flex-start. */
  align-items: center;
}
.rv-eac__heading {
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
  /* Narrow enough to break as "Intelligent and Safe" / "Teaching Assistant"
     rather than filling the column. The lockup above is 200px wide, well inside
     this, so it is never what sets the width. */
  max-width: 22ch;
}
/* The EduAI lockup, sized by the same rule as the VisionX one (.rv-vx__logo):
   its wordmark sets at the SAME cap height as the copy beneath it, so the two
   sections' heads are built the same way.

     the wordmark's caps run y 26.007..55.635 of the 67-unit box, i.e. 0.4422 of it
     so height = 0.727 / 0.4422 = 1.644em, and the width follows the 202:67 aspect

   display: block is what the site's base `img { display: block }` already gives;
   stated here so it reads as intended rather than inherited. */
.rv-eac__logo {
  display: block;
  height: 1.644em;
  width: 4.957em;
  /* Bottom: 0.375em lands the visible gap at ~22px, matching the VisionX head's
     0.76-of-cap-height gap. It needs more than that head does because this
     artwork has almost no padding under its icon (ink to y 66.6 of 67), where the
     VisionX icon stops at 74.6 of 79.

     Left: -0.083em pulls the mark out to sit flush with the copy beneath it.
     MEASURED, not computed: the artwork's own left padding is 6.5 of 202 units,
     but the copy's first glyph carries left side bearing of its own, and the net
     offset on the render was 3.3px at the 40px desktop size. Computing it from the
     SVG alone would have over-corrected by ~3px. */
  margin: 0 0 0.375em -0.083em;
}
.rv-eac__intro {
  flex: 0 1 470px;
  color: var(--rv-muted);
  font-size: var(--rv-fs-lede);
  line-height: 1.55;
  padding-top: 6px;
}

/* Two columns in the design's own proportion — the panel takes 58% of the
   content width and the list 35%, i.e. 1.667 : 1 with the gap between.
   An `auto` left track sized from the panel's own aspect instead squeezed the
   list to its 260px minimum while the panel ran to 940px. */
.rv-eac__body {
  max-width: var(--rv-eac-max);
  margin: 0 auto;
  padding: 0 24px;
  display: grid;
  grid-template-columns: minmax(0, 1.667fr) minmax(0, 1fr);
  gap: 48px;
  /* The ROW height comes from the panel alone: the list's track is taken out
     of flow below, so the list contributes no intrinsic height and simply
     stretches to whatever the panel is. That is what keeps the two columns
     exactly level without a shared height variable to maintain. */
  align-items: stretch;
}

/* ----- The panel ----- */
.rv-eac__stage {
  min-width: 0; /* let the grid track shrink rather than overflow */
}
.rv-eac__frame {
  position: relative;
  /* Left-aligned, not centred: the panel's left edge has to line up with the
     heading above it. */
  margin-inline: 0;
  /* Column width normally governs; the vh term only bites on a short window,
     keeping the section inside the fold there. */
  width: min(100%, calc(var(--rv-eac-media-h) * 1.5667));
  /* THE DEVICE, not the artwork. The asset is 1410x900 but the solid panel only
     occupies x 83..1324, y 67..832 of it — the rest is transparent margin. Left
     as-is that margin pushed the visible device 38px right of the heading and
     32px below the top of the card list. Cropping the box to the device instead
     of nudging the neighbours means both edges line up on their own.

     1278x811 is the device's FULL silhouette (alpha > 8), shadow included, so
     mapping it to this box clips nothing of the artwork while still putting the
     device's own edges on the box's edges.

     THE ZOOM IS CLIPPED TO THIS BOX. Every clip opens by zooming into the screen
     until the UI fills the whole 1410x900 artwork, which is wider and taller than
     the device silhouette this box is cut to.

     That bleed was allowed to show for a while, on the reasoning that clipping
     "sliced the outer ~10% off the zoom". Seeing it on the page settled it the
     other way: measured at 1440px, the opaque zoom reached 42px left, 36px above
     and 29px right of a 690x438 panel, so for a second or so the section stopped
     reading as a device on a page and became a dark rectangle growing out of its
     own frame and pushing toward the card list. A cropped zoom is a much smaller
     price than that.

     So the outer ~10% of the zoom IS cropped, deliberately. The clip still plays
     in full; it is only the moment of maximum zoom that loses its outer edge, and
     the device's own bezel is what the eye reads as the boundary anyway.

     Keeping the BOX at the device silhouette (rather than growing it to the
     artwork) is what keeps the device's own edges on the heading and the card
     list — the alignment this crop was introduced for is unchanged, because the
     media's scale and offsets are untouched. Growing the box to the full artwork
     would contain the zoom too, but it puts the device 38px right of the heading
     and 32px below the list, which is the very thing the crop exists to fix.

     Two earlier attempts at the box itself were wrong and should not be retried:
       - alpha > 180 (1242x766) cropped into the DEVICE. Its lower bezel is a
         light grey with soft alpha, fell below that threshold, and got shaved off
       - the 1410x900 full-artwork box put the device 38px right of the heading
         and 32px below the top of the card list
     `overflow: visible` here was wrong twice, for different reasons: once when a
     stale hold canvas painted an opaque frame behind every clip and the bleed
     carried it over the list (fixed separately, see clearHoldSoon in
     revamp-home.js), and again for the zoom above.

     `clip`, not `hidden`: it contains the media without making the panel a scroll
     container, matching the choice on .rv-eac. That section-level overflow-x: clip
     is now belt-and-braces rather than load-bearing — nothing bleeds this far any
     more — but it is left in place because it also guards the phone layout. */
  aspect-ratio: 1278 / 811;
  overflow: clip;
}
/* Scale the artwork up and pull it back by its own margin so the device fills
   the cropped box exactly. 1410/1278 and 900/811 keep the original ratio, so
   nothing is stretched.

   max-width: none is load-bearing, not tidying. The global `img { max-width:
   100% }` clamped the width back to the frame's 100% while the height stayed at
   110.974%, which squeezed the panel to a 1.42 aspect against its natural
   1.5667 — a 0.906x horizontal crush, exactly the reciprocal of the scale-up.
   The same global rule caught the hero photo earlier; any element deliberately
   sized past its container in this file needs it. */
/* Scales the 1410x900 artwork so its full device silhouette (1278x811 at 78,66)
   fills the frame: 1410/1278 = 110.329% wide, 900/811 = 110.974% tall, offset by
   78/1410 and 66/900 of those. Recompute all four together, and the frame's
   aspect-ratio with them, if the clips are re-exported. */
.rv-eac__poster,
.rv-eac__video {
  position: absolute;
  display: block;
  width: 110.329%;
  height: 110.974%;
  max-width: none;
  left: -6.103%;
  top: -8.138%;
  object-fit: fill;
}
/* The video fades in over the poster only once it is genuinely playing, so a
   slow network or a refused autoplay leaves the poster showing rather than a
   hole in the layout. */
.rv-eac__video {
  opacity: 0;
  transition: opacity 0.35s linear;
}
.rv-eac__video.is-playing {
  opacity: 1;
}

/* ----- The list ----- */
.rv-eac__list {
  position: relative;
  min-height: 0;
  overflow: hidden;
  /* NO BOTTOM FADE. There used to be a 96px mask here, so the list read as
     continuing past its edge. The window is now an exact four cards — one above
     the live one, the live one, and two below — and all four are meant to be read
     and clicked, so fading the last of them would dim a control. Nothing peeks
     past the fourth card for a fade to soften. */
}
.rv-eac__track {
  /* Out of flow so the list has no intrinsic height and inherits the panel's
     via the grid row — see .rv-eac__body. */
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  /* Set by initEduaiCards as the list advances. The transition is the "shift
     up" — long and eased enough to read as a deliberate move rather than a
     jump, which is the whole point of the interaction. */
  transform: translateY(0);
  transition: transform 0.62s cubic-bezier(0.65, 0, 0.35, 1);
}

.rv-eac__card {
  position: relative;
  /* Gap between the copy and the timer rule — deliberately tight, so the rule
     reads as belonging to the card above it rather than floating between two. */
  padding-bottom: 10px;
  /* 14px, down from 26px, so FOUR cards fit the window the panel's height gives
     the list. Measured: the cards are a uniform 97px at 1440px and 98px at
     1728px against a 438px viewport, which leaves (438 - 4x98)/3 = 15px to spend
     between them; 14px clears it at every width down to 1024px. Raising this
     drops the fourth card out of view and with it half the way down the list. */
  margin-bottom: 14px;
}
/* Idle baseline. Sits under every card including the live one, so the blue
   timer fill has a track to run along instead of appearing out of nothing. */
.rv-eac__card::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--rv-eac-sep);
}

.rv-eac__btn {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  font: inherit;
  color: var(--rv-eac-dim);
  transition: color 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.rv-eac__btn:focus-visible {
  outline: 2px solid var(--rv-eac-rule);
  outline-offset: 4px;
}
.rv-eac__title {
  font-size: var(--rv-fs-title);
  font-weight: var(--rv-fw-title);
  line-height: 1.25;
  color: inherit;
  transition: color 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.rv-eac__desc {
  font-size: var(--rv-fs-body);
  line-height: 1.5;
  color: inherit;
}
.rv-eac__btn[aria-current='true'] .rv-eac__title {
  color: var(--rv-navy);
}
.rv-eac__btn[aria-current='true'] .rv-eac__desc {
  color: var(--rv-muted);
}
/* Hover hints only on cards that are not already live, and only where a real
   pointer exists — on touch this would stick after a tap. */
@media (hover: hover) {
  .rv-eac__btn:hover:not([aria-current='true']) {
    color: var(--rv-muted);
  }
}

/* ----- Dwell timer -----
   Rides on top of the idle baseline and fills left to right. The duration is
   set inline by initEduaiCards from the macro's dwell_ms, so the bar and the
   advance are driven by the same number and cannot drift apart. */
.rv-eac__bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  z-index: 1;
  background: var(--rv-eac-rule);
  transform: scaleX(0);
  transform-origin: left center;
}
.rv-eac__bar.is-running {
  transform: scaleX(1);
}

/* ----- Arrows (phones only) ----- */
/* Step controls, PHONES ONLY (flex in the 640px block below). There the list is a
   horizontal strip showing one card, so there is nothing to click to go back and
   the arrows are the control.

   Deliberately not shown on desktop. They were, briefly, to fix the list having
   no way back — but sitting under the panel, far from the cards they moved, they
   read as unrelated page furniture. The vertical list solves it in the layout
   instead: the live card sits in the middle of a window of four, so the card above
   it and the two below are all visible and clickable, and the direction is
   obvious. See the click handler in initEduaiCards. */
.rv-eac__nav {
  display: none;
  margin-top: 18px;
  justify-content: flex-end;
  gap: 14px;
}
.rv-eac__arrow {
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  border: 1px solid #c9d6ee;
  background: #ffffff;
  color: var(--rv-navy);
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s;
}
.rv-eac__arrow:hover {
  background: #f0f5fd;
  border-color: #9fb8e4;
}
.rv-eac__arrow:focus-visible {
  outline: 2px solid var(--rv-eac-rule);
  outline-offset: 2px;
}
.rv-eac__arrow svg {
  width: 20px;
  height: 20px;
}

@media (max-width: 1024px) {
  .rv-eac {
    padding: 32px 0 40px;
    /* Stacked, so the panel no longer has to share the fold with the list. */
    --rv-eac-media-h: clamp(220px, 42vw, 420px);
  }
  .rv-eac__head {
    flex-direction: column;
    gap: 14px;
    margin-bottom: 56px;
    /* Column from here down: back to the top/left edge. The 640px block below
       overrides this again with center, which is the centred phone head. */
    align-items: flex-start;
  }
  .rv-eac__heading,
  .rv-eac__intro {
    max-width: none;
    flex: none;
  }
  .rv-eac__body {
    grid-template-columns: minmax(0, 1fr);
    gap: 28px;
    /* NOT justify-items: center. That sizes each grid item to its content, so
       .rv-eac__stage had no resolved width and the frame's `min(100%, …)`
       collapsed the panel to 0x0. The items stay stretched and their contents
       are centred with auto margins instead. */
  }
  .rv-eac__stage,
  .rv-eac__list {
    width: 100%;
    max-width: 640px;
    /* Left, not centred: the heading is still left-aligned at this width, and
       an auto margin here put the panel 66px to the right of it. Below 640px
       both are narrower than the cap anyway, so the centred heading there is
       unaffected. */
    margin-inline: 0;
  }
  .rv-eac__list {
    /* Single column here, so there is no panel row to inherit a height from —
       state one directly.

       340px, up from 260px: the list shows a WINDOW OF FOUR (the card above the
       live one, the live one, and the next two — see initEduaiCards), and at 260px
       only three fitted, so the second card below the live one was cut off and with
       it half the way down the list.

       340 rather than 4 x 63 + 3 x 14 = 294, because THE CARDS ARE NOT ALL THE SAME
       HEIGHT here. At these widths five descriptions fit one line (63px) and two
       wrap to a second (84px) — AI Whiteboards and Class Recap. The track rotates,
       so any four consecutive cards can be in the window, and those two sit three
       apart, which means both can appear at once: 84 + 63 + 63 + 84 plus three 14px
       gaps = 336px is the worst case. 340 clears it.

       Re-measure if a description's length changes: a third two-line card, or one
       running to three lines, moves this number. */
    height: 340px;
  }
}

@media (max-width: 640px) {
  .rv-eac {
    padding: 28px 0 36px;
    --rv-eac-media-h: clamp(180px, 56vw, 320px);
  }
  .rv-eac__head,
  .rv-eac__body {
    padding-inline: 16px;
  }
  /* Phones centre the heading and lede. The head is already a flex column at
     this width, so align-items handles the boxes and text-align the lines. */
  .rv-eac__head {
    text-align: center;
    align-items: center;
  }
  /* Centred head: text-align does not move a block element, so the lockup needs
     its own centring — which also cancels the optical left pull above, since that
     is only right when the head is left-aligned. */
  .rv-eac__logo {
    margin-inline: auto;
  }
  /* The panel fills the column here rather than being capped by
     --rv-eac-media-h. Left-aligned inside a wider stage it put all 16px of the
     leftover slack on the right, which read as off-centre under a centred
     heading; filling gives equal 16px gutters and lines its left edge up with
     the cards below. (Above 640px the cap stays: the heading is left-aligned
     there, so the slack sitting to the right is correct.) */
  .rv-eac__frame {
    width: 100%;
  }

  /* ----- Phones: the strip runs SIDEWAYS -----
     One card in view with the next peeking, advanced by the same dwell timer
     (initEduaiCards switches its translate axis to match) plus arrows, since a
     single visible card gives no other clue that there are five more. */
  .rv-eac__list {
    height: auto;
    overflow: hidden;
    /* No fade on phones: the strip runs sideways, so a bottom fade would cut
       across the live card. The peeking next card and the arrows already say
       there is more. */
    -webkit-mask-image: none;
    mask-image: none;
  }
  .rv-eac__track {
    /* Back into flow: with one row of cards the list takes its height from the
       track, so there is no panel row to inherit and nothing to take out. */
    position: static;
    display: flex;
    align-items: flex-start;
  }
  .rv-eac__card {
    /* The peek is the affordance — a full-width card would look like the end
       of the list. */
    flex: 0 0 84%;
    margin-right: 16px;
    margin-bottom: 0;
    padding-bottom: 10px;
  }
  .rv-eac__nav {
    display: flex;
  }
}

/* Reduced motion: initEduaiCards does not auto-advance at all here, so there
   is no timer to draw and no slide to soften — the list is a plain set of
   buttons and the bar just marks the live one. */
@media (prefers-reduced-motion: reduce) {
  .rv-eac__track,
  .rv-eac__btn,
  .rv-eac__title,
  .rv-eac__video {
    transition: none;
  }
  .rv-eac__bar {
    transition: none;
  }
}

/* ==========================================================================
   Section 4 — Interactive whiteboard (pinned 3-stage reveal)

   One device, built up in place. The pin holds it still while three scroll
   stages add the toolbar, then the sphere + equation, then the language row.
   Reveals are driven by CLASSES on the section (is-live / is-s1..s3) rather
   than per-frame inline styles: the JS only decides which stage the scroll is
   in, and CSS transitions do the tweening. That also means reduced motion just
   gets the finished state for free.
   ========================================================================== */
.rv-wb {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;

  /* 1% of the device's width, so every size inside the screen scales with the
     panel instead of needing a value per breakpoint. The device is
     min(1100px, column) wide and the column is 100vw - 48px, hence the min().
     Container queries would express this directly but are still shaky on the
     older iOS versions this section has to survive. */
  --rv-wb-u: min(11px, calc((100vw - 48px) / 100));

  /* Screen rect, measured off ifp.webp (2216x1398): x 83..2134, y 94..1257.
     Every overlay is positioned against these, so the composition holds
     together at any device size. */
  --rv-wb-scr-l: 3.745%;
  --rv-wb-scr-t: 6.724%;
  --rv-wb-scr-w: 92.599%;
  --rv-wb-scr-h: 83.262%;

  --rv-wb-ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* The nav is position: fixed, so a pin stuck to the viewport top spends its
     first nav-height underneath it — which cut the top line off both the heading
     and the subhead. Reads the nav's own geometry token so the two cannot drift
     when the nav's height changes. */
  --rv-wb-nav: var(--rv-nav-space);

  /* One width for the intro AND the stage, so the heading stays aligned with the
     left language column and the subhead with the right one. 1200px is the
     shared page margin every other section uses — widening this one to fit the
     new button columns made the section visibly bulge past its neighbours, so
     the device gives up the width instead. */
  --rv-wb-max: 1200px;
  /* Width of each language column. Floored so the labels never crush, capped
     tightly because whatever these take comes straight off the device: at a
     186px cap the panel lost ~130px on a wide screen for no gain — the labels
     are all short words. */
  --rv-wb-lang-col: clamp(84px, 9.5vw, 152px);

  /* Per-step delays for the one cascade that opens the board, all in one place
     so the order is readable as a list instead of scattered down the file.
     Measured from the moment the stage classes land — see initWhiteboardReveal.
     Everything here is shared by every breakpoint: the sequence is the same
     everywhere now, only the geometry around it changes. */
  --rv-wb-d-toolbar: 0.1s;
  --rv-wb-d-tools: 0.25s;
  --rv-wb-d-chip-pens: 0.42s;
  --rv-wb-d-sphere: 0.52s;
  --rv-wb-d-lesson: 0.6s;
  --rv-wb-d-chip-3d: 0.7s;
  --rv-wb-d-chip-ocr: 0.78s;
  /* The equation writes itself over 1.5s; this waits for the block it sits in. */
  --rv-wb-d-write: 0.85s;
  --rv-wb-d-copy: 1.05s;
  /* Language buttons: the first one, then one every --rv-wb-lang-step. */
  --rv-wb-d-langs: 1.15s;
  --rv-wb-lang-step: 0.09s;
  --rv-wb-d-note: 2s;

  position: relative;
  /* Just its content, one screen of it. This was `height: 320vh` — a track for
     the pin to stick through, which is the scrolling the feedback objected to.
     The build-up it paid for now comes from the delays above. */
  height: auto;
  padding: 40px 0 48px;
  background: #ffffff;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}

/* Ordinary wrapper (was .rv-wb__pin, position: sticky). It stays for the side
   gutter and for `overflow: hidden`, which the device's entrance depends on.

   HORIZONTAL PADDING ONLY. It used to carry 20px top and bottom as well, which
   made sense when it was a full-height pin and the section around it had no
   padding of its own. Now the section has its own, and the two together came to
   96px of vertical chrome — enough that at a 900px viewport the heading ended up
   tucked under the fixed nav. */
.rv-wb__inner {
  overflow: hidden;
  padding: 0 24px;
}

/* Intro — same shape as the other sections' heads */
.rv-wb__intro {
  width: 100%;
  max-width: var(--rv-wb-max);
  margin: 0 auto 20px;
  display: flex;
  gap: 40px;
  justify-content: space-between;
  align-items: flex-start;
}
.rv-wb__heading {
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
  max-width: 24ch;
}
/* One sentence per line. The macro wraps each sentence of the heading in one of
   these, so the break lands on the full stop at every width instead of wherever
   the column happens to run out — which was splitting "Perfected in classrooms."
   across two lines on a phone. A one-sentence heading renders as a single block
   and looks no different. */
.rv-wb__heading-line {
  display: block;
}
.rv-wb__subhead {
  flex: 0 1 380px;
  color: var(--rv-muted);
  font-size: var(--rv-fs-lede);
  line-height: 1.5;
  padding-top: 6px;
}

/* The stage: four language buttons down the left, the device, four down the
   right, and the "90+ languages" line spanning underneath all three. */
.rv-wb__stage {
  width: 100%;
  max-width: var(--rv-wb-max);
  margin: 0 auto;
  display: grid;
  grid-template-columns:
    minmax(0, var(--rv-wb-lang-col))
    minmax(0, 1fr)
    minmax(0, var(--rv-wb-lang-col));
  align-items: center;
  column-gap: calc(var(--rv-wb-u) * 2);
}

/* ----- The device -----
   Entrance: zooms up once the section reaches the viewport. This is the only
   reveal not tied to a scroll stage — it fires on intersection, so the panel is
   already settled by the time the first stage begins. */
.rv-wb__device {
  position: relative;
  grid-column: 2;
  /* Capped by the FOLD, not just the column. Nothing pins any more, but the
     whole point of the section is that it reads in one screen, so the device
     still gives up width rather than push the note below the fold. 250px is the
     measured chrome — padding + intro + the two gaps + the note — and 1.5852 is
     the device's own 2216:1398 aspect turning height back into width. It was
     300px when the language buttons sat in a row BELOW the device; they now flank
     it, so that row's height is no longer part of the reserve. */
  width: min(
    100%,
    1100px,
    calc((100vh - var(--rv-wb-nav) - 250px) * 1.5852)
  );
  margin: 0 auto;
  /* Entrance is a zoom-OUT that settles into place, the same depth move the
     toolbar and the sphere make later: the panel arrives oversized and soft, as
     if it were in front of the page and too close to focus on, then shrinks to
     fit. It used to grow from scale(0.82), which read as the panel receding
     into the page rather than arriving out of it.
     At 1.4 the device overruns its wrapper, and .rv-wb__inner is overflow:
     hidden — that clipping is wanted, it is what makes it look like it is coming
     through the frame rather than merely getting larger. */
  opacity: 0;
  transform: scale(1.4);
  transform-origin: 50% 50%;
  filter: blur(7px);
  transition: opacity 0.45s var(--rv-wb-ease),
    transform 1.15s var(--rv-wb-ease), filter 0.9s var(--rv-wb-ease);
}
.rv-wb.is-live .rv-wb__device {
  opacity: 1;
  transform: scale(1);
  filter: blur(0);
}
.rv-wb__frame {
  display: block;
  width: 100%;
  height: auto;
}

/* The screen, as a positioning context for everything drawn on it */
.rv-wb__screen {
  position: absolute;
  left: var(--rv-wb-scr-l);
  top: var(--rv-wb-scr-t);
  width: var(--rv-wb-scr-w);
  height: var(--rv-wb-scr-h);
  overflow: hidden;
  color: #ffffff;
  font-size: calc(var(--rv-wb-u) * 1.5);
}

/* Shared reveal: each element declares its own resting offset, and the stage
   class removes it. Kept in one rule so the timing cannot drift apart. */
.rv-wb__toolbar,
.rv-wb__tools,
.rv-wb__sphere,
.rv-wb__chip,
.rv-wb__lesson {
  opacity: 0;
  transition: opacity 0.6s var(--rv-wb-ease), transform 0.7s var(--rv-wb-ease);
}

/* ---- initial state: the Whiteboard app splash ----
   Holds the screen from the device's entrance until stage 1, which is what
   removes the empty black panel at the start of the sequence. 52% of the screen
   width is the icon-plus-wordmark's share in the design.

   Both state rules below have the same specificity (0,3,0), so SOURCE ORDER
   decides which wins once .is-s1 joins .is-live — is-s1 has to stay second.
   Under reduced motion the JS adds every stage class at once, so the splash is
   hidden there too, which is right: the finished composition has no splash. */
.rv-wb__splash {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 52%;
  height: auto;
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity 0.55s var(--rv-wb-ease);
}
.rv-wb.is-live .rv-wb__splash {
  opacity: 1;
}
.rv-wb.is-s1 .rv-wb__splash {
  opacity: 0;
}

/* ---- stage 1: toolbar + pen tools ---- */
.rv-wb__toolbar {
  position: absolute;
  left: 1.5%;
  bottom: 2.5%;
  width: 97%;
  height: auto;
  max-width: none;
  /* Comes OUT of the screen and zooms back to fit: it starts oversized and
     lifted toward the middle of the board — as if it were in front of the glass
     and closer to the viewer — then shrinks and settles into the toolbar slot.
     The blur sells the depth: something that close would be out of focus.
     Origin is the centre so the shrink converges on the resting position
     instead of sliding into it. */
  transform: translateY(-34%) scale(1.85);
  transform-origin: 50% 50%;
  filter: blur(3px);
  transition: opacity 0.5s var(--rv-wb-ease),
    transform 1.05s var(--rv-wb-ease), filter 0.8s var(--rv-wb-ease);
  transition-delay: var(--rv-wb-d-toolbar);
}
.rv-wb.is-s1 .rv-wb__toolbar {
  opacity: 1;
  transform: translateY(0) scale(1);
  filter: blur(0);
}

.rv-wb__tools {
  position: absolute;
  left: 6%;
  bottom: 13%;
  width: 30%;
  height: auto;
  max-width: none;
  transform: translateY(12%) scale(0.94);
  transition-delay: var(--rv-wb-d-tools);
}
.rv-wb.is-s1 .rv-wb__tools {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* ---- stage 2: sphere + equation ---- */
.rv-wb__sphere {
  position: absolute;
  right: 2%;
  top: 5%;
  height: 78%;
  width: auto;
  max-width: none;
  /* Same depth move as the toolbar: it starts oversized and pulled toward the
     middle of the board — out in front of the glass — then shrinks back and
     seats itself on the screen. Blur for the same reason: this close it would
     be out of focus. Origin is its own centre so the shrink converges on the
     resting spot rather than sliding across to it. */
  transform: translate(-11%, 7%) scale(1.7);
  transform-origin: 50% 50%;
  filter: blur(4px);
  transition: opacity 0.55s var(--rv-wb-ease),
    transform 1.15s var(--rv-wb-ease), filter 0.85s var(--rv-wb-ease);
  transition-delay: var(--rv-wb-d-sphere);
}
.rv-wb.is-s2 .rv-wb__sphere {
  opacity: 1;
  transform: translate(0, 0) scale(1);
  filter: blur(0);
}

.rv-wb__lesson {
  position: absolute;
  left: 4.5%;
  top: 9%;
  width: 45%;
  transition-delay: var(--rv-wb-d-lesson);
}
.rv-wb.is-s2 .rv-wb__lesson {
  opacity: 1;
}

.rv-wb__eq {
  margin: 0 0 0.7em;
  font-size: calc(var(--rv-wb-u) * 3.1);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.rv-wb__eq sup {
  /* vertical-align: super lifted these so far they read as tick marks at board
     size; an explicit offset puts the exponent where a maths setter would. */
  font-size: 0.6em;
  vertical-align: baseline;
  position: relative;
  top: -0.46em;
  line-height: 0;
}
/* "Written" onto the board: a left-to-right wipe with a nib travelling at its
   edge. A true per-glyph stroke animation would need vector outlines of the
   glyphs, which the typeset equation in the design does not give us — this is
   the standard simulation and it survives a font or language change. */
.rv-wb__eq-ink {
  position: relative;
  display: inline-block;
  clip-path: inset(0 100% 0 0);
}
.rv-wb.is-s2 .rv-wb__eq-ink {
  animation: rv-wb-write 1.5s var(--rv-wb-ease) var(--rv-wb-d-write) forwards;
}
.rv-wb__eq-ink::after {
  content: '';
  position: absolute;
  top: 8%;
  bottom: 8%;
  left: 0;
  width: 0.06em;
  background: #8ab6ff;
  opacity: 0;
}
.rv-wb.is-s2 .rv-wb__eq-ink::after {
  animation: rv-wb-nib 1.5s var(--rv-wb-ease) var(--rv-wb-d-write) forwards;
}
@keyframes rv-wb-write {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}
@keyframes rv-wb-nib {
  0%   { left: 0;    opacity: 1; }
  85%  { opacity: 1; }
  100% { left: 100%; opacity: 0; }
}

.rv-wb__copy {
  font-size: calc(var(--rv-wb-u) * 1.16);
  line-height: 1.5;
  color: #d7e2f3;
  opacity: 0;
  transition: opacity 0.7s var(--rv-wb-ease) var(--rv-wb-d-copy);
}
.rv-wb.is-s2 .rv-wb__copy {
  opacity: 1;
}
.rv-wb__copy p {
  margin: 0 0 0.35em;
}
.rv-wb__copy p:last-child {
  margin-bottom: 0;
}

/* ---- chips ---- */
.rv-wb__chip {
  position: absolute;
  z-index: 2;
  padding: 0.42em 0.85em;
  border-radius: 999px;
  background: #ffffff;
  color: var(--rv-navy);
  font-size: calc(var(--rv-wb-u) * 0.95);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  box-shadow: 0 0.2em 0.7em rgba(6, 18, 40, 0.35);
  transform: scale(0.8);
}
.rv-wb__chip--pens {
  left: 33%;
  bottom: 12%;
  transition-delay: var(--rv-wb-d-chip-pens);
}
.rv-wb__chip--3d {
  right: 26%;
  /* 13.5%, down from 8%, to clear the sphere's "z" axis label. The letter sits at
     y 18..38 of the 500x500 render, and the render is placed at top: 5% with
     height: 78%, which puts the glyph at 7.81%..10.93% of the screen — straight
     through the chip's old 8%..12.59% band. At 13.5% the chip starts 2.6% below
     the glyph and its bottom lands at ~18%, still well clear of the sphere's own
     top edge at 22.9%. Re-derive all three numbers together if the sphere render
     or its top/height changes. */
  top: 13.5%;
  transition-delay: var(--rv-wb-d-chip-3d);
}
.rv-wb__chip--ocr {
  left: 4.5%;
  top: 3%;
  transition-delay: var(--rv-wb-d-chip-ocr);
}
.rv-wb.is-s1 .rv-wb__chip--pens,
.rv-wb.is-s2 .rv-wb__chip--3d,
.rv-wb.is-s2 .rv-wb__chip--ocr {
  opacity: 1;
  transform: scale(1);
}

/* ---- stage 3: the two language columns ----
   Two elements share this class, one per side of the device. --start / --end only
   place them in the grid.

   THE COLUMNS THEMSELVES NO LONGER FADE. They used to reveal as two blocks, which
   put all eight buttons on screen in one movement; the buttons now arrive ONE BY
   ONE (see .rv-wb__lang below), because the feedback was that visitors did not
   read them as clickable. Something that assembles itself in front of you reads as
   a control in a way a block that is simply present does not. Keep the reveal on
   the buttons — moving it back up here would undo that. */
.rv-wb__langs {
  text-align: center;
}
.rv-wb__langs--start {
  grid-column: 1;
}
.rv-wb__langs--end {
  grid-column: 3;
}
.rv-wb__lang-row {
  display: flex;
  /* A column beside the device, not a row beneath it. */
  flex-direction: column;
  gap: calc(var(--rv-wb-u) * 1.5);
}
.rv-wb__lang {
  position: relative;
  /* Fills its column: the buttons all end up the same width whatever the label,
     which is what makes the two stacks read as tidy edges either side of the
     device. min-width would fight the column's clamp on a narrow viewport. */
  width: 100%;
  min-width: 0;
  padding: 0.75em 1em;
  border: 1px solid #c9d6ee;
  border-radius: 10px;
  background: #ffffff;
  color: var(--rv-navy);
  font: inherit;
  font-size: calc(var(--rv-wb-u) * 1.45);
  font-weight: 600;
  cursor: pointer;

  /* This button's own place in the queue. --rv-wb-lang-i is set per button by the
     macro (0-based, running across both columns); the fallback keeps a button
     without it visible rather than stuck at opacity 0. */
  --rv-wb-lang-delay: calc(
    var(--rv-wb-d-langs) + var(--rv-wb-lang-i, 0) * var(--rv-wb-lang-step)
  );
  opacity: 0;
  transform: translateY(10px) scale(0.96);
  /* PER-PROPERTY delays, deliberately. A single `transition-delay` would also
     delay background-color and border-color, so the press state on the eighth
     button would answer the tap the better part of a second late. Only the two
     reveal properties wait. */
  transition: background-color 0.3s, border-color 0.3s, color 0.3s,
    opacity 0.5s var(--rv-wb-ease) var(--rv-wb-lang-delay),
    transform 0.5s var(--rv-wb-ease) var(--rv-wb-lang-delay);
}
.rv-wb.is-s3 .rv-wb__lang {
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* ---- the nudge ----
   Feedback: visitors did not know the language buttons could be clicked. So once
   they have all arrived, a ring pulses out of each in turn — a wave travelling
   down the left column and on down the right, three times, then it stops.

   Why it stops: a control that pulses forever stops reading as an invitation and
   starts reading as a decoration, and it competes with the board itself for
   attention. Three passes is enough to be seen without becoming furniture. It also
   stops the instant anyone clicks or focuses a button — see stopAttract in
   revamp-home.js.

   box-shadow and border-color only, NOT transform: the ring has to expand outside
   the button (a transform would move the label, and a scale on a bordered box
   reads as a wobble). The buttons are 10px apart at the tightest, and the ring
   tops out at 5px, so two neighbouring pulses never touch.

   The pressed button is excluded — it is already solid blue, and pulsing the one
   that is currently in effect would suggest it needs pressing again. */
@keyframes rv-wb-lang-nudge {
  0% {
    box-shadow: 0 0 0 0 rgba(29, 161, 242, 0);
    border-color: #c9d6ee;
  }
  35% {
    box-shadow: 0 0 0 5px rgba(29, 161, 242, 0.18);
    border-color: #1da1f2;
  }
  70%,
  100% {
    box-shadow: 0 0 0 0 rgba(29, 161, 242, 0);
    border-color: #c9d6ee;
  }
}
.rv-wb.is-attracting .rv-wb__lang:not([aria-pressed='true']) {
  /* Stagger is wider than the arrival stagger so the wave reads as a separate
     gesture rather than a continuation of the buttons appearing. */
  animation: rv-wb-lang-nudge 2.2s var(--rv-wb-ease)
    calc(var(--rv-wb-lang-i, 0) * 0.11s) 3;
}
/* Brand sky blue: the value behind --tm-color-bl-2 in the shared tm-style
   sheet. Was an off-brand #2f8ff5. Also used for this button's focus ring and
   its hover trace, so the whole control speaks one colour.
   NOTE white-on-#1da1f2 measures 2.8:1, under the 4.5:1 WCAG AA minimum for
   text this size — flagged with the brand rather than silently corrected here,
   since the colour pairing is a brand decision, not a local one. */
.rv-wb__lang[aria-pressed='true'] {
  background: #1da1f2;
  border-color: #1da1f2;
  color: #ffffff;
}
.rv-wb__lang:focus-visible {
  outline: 2px solid #1da1f2;
  outline-offset: 3px;
}
.rv-wb__lang-label {
  position: relative;
  z-index: 1;
}
/* The trace: a rounded rect whose stroke draws itself round the button on
   hover or focus, as a hint that these are interactive. pathLength normalises
   the dash maths so one value works whatever the button's size. */
.rv-wb__lang-trace {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.rv-wb__lang-trace rect {
  fill: none;
  stroke: #1da1f2;
  stroke-width: 2;
  stroke-dasharray: 1 1;
  stroke-dashoffset: 1;
  opacity: 0;
  transition: stroke-dashoffset 0.65s var(--rv-wb-ease), opacity 0.2s;
}
@media (hover: hover) {
  .rv-wb__lang:hover:not([aria-pressed='true']) .rv-wb__lang-trace rect {
    opacity: 1;
    stroke-dashoffset: 0;
  }
}
.rv-wb__lang:focus-visible .rv-wb__lang-trace rect {
  opacity: 1;
  stroke-dashoffset: 0;
}

/* The note spans all three columns on its own row under the device. It used to
   be a child of .rv-wb__langs and inherited that block's reveal; standing alone
   it needs its own copy of it. */
.rv-wb__lang-note {
  grid-column: 1 / -1;
  margin: calc(var(--rv-wb-u) * 1.5) 0 0;
  text-align: center;
  color: var(--rv-navy);
  /* Sized off --rv-wb-u like the rest of the section, so it tracks the device.
     2.1 puts it at ~23px on a desktop — it is the one line of copy under the
     whole composition and was getting lost at 1.6. */
  font-size: calc(var(--rv-wb-u) * 2.1);
  font-weight: 500;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.6s var(--rv-wb-ease), transform 0.6s var(--rv-wb-ease);
  transition-delay: var(--rv-wb-d-note);
}
.rv-wb.is-s3 .rv-wb__lang-note {
  opacity: 1;
  transform: translateY(0);
}

/* The shorter nav below 1194px is handled by the :root override above, so the
   whiteboard's offset follows automatically — no breakpoint needed here. */

@media (max-width: 1024px) {
  .rv-wb {
    --rv-wb-u: min(11px, calc((100vw - 32px) / 100));
  }
  .rv-wb__inner {
    padding: 0 16px;
  }
  .rv-wb__intro {
    flex-direction: column;
    gap: 12px;
    margin-bottom: 20px;
  }
  .rv-wb__heading,
  .rv-wb__subhead {
    max-width: none;
    flex: none;
  }
}

@media (max-width: 640px) {
  .rv-wb {
    padding: 32px 0 40px;
  }
  .rv-wb__inner {
    padding: 0 16px;
  }

  /* NOTHING ABOUT THE TIMING CHANGES HERE any more. This block used to carry the
     whole cascade, because phones were the only breakpoint that played the reveal
     on entry — above 640px it was gated on scroll. Now every width plays the same
     sequence from the --rv-wb-d-* tokens, and what is left here is geometry. */
  .rv-wb__lesson {
    /* 50%, up from 45%, to absorb the extra lines the larger copy below needs.
       50% is the limit: at 54% the text's right edge came within 5px of the
       sphere's fill, and the sphere sits alongside this block at these widths. */
    width: 50%;
  }
  .rv-wb__copy {
    /* Floored in px, for the same reason the language buttons and the note are:
       --rv-wb-u tracks the DEVICE, which on a 390px phone put this copy at
       4.2px — too small to read at all. The outer max() keeps the proportional
       size wherever it is already the larger of the two (7.05px at 640px, and up
       from there), so this only lifts the narrow end.

       THE FLOOR IS THE CEILING THE BOARD ALLOWS, measured per width rather than
       guessed. The binding constraint is .rv-wb__tools, the writing-tools panel
       that sits under this copy from stage 1 onward: bigger type means more lines
       in a fixed-width column, and the lines run into it.
         at 390px  7px leaves 21px clear, 8px leaves 2px, 9px overlaps
         at 320px  6px leaves  5px clear, 6.5px leaves 1px, 7px overlaps by 13px
       Hence a ramp, not a constant: `2.5vw - 2px` is 6px at 320 and 7px at 360,
       and the min/max pin it to that range either side. A flat 7px overlapped the
       tools panel on a 320px phone. Re-measure both rows above against
       .rv-wb__tools and the sphere's fill if either moves. */
    font-size: max(
      calc(var(--rv-wb-u) * 1.16),
      min(7px, max(6px, calc(2.5vw - 2px)))
    );
  }
  .rv-wb__intro {
    text-align: center;
    align-items: center;
    margin-bottom: 14px;
  }
  /* Phones cannot afford a column either side of the device — at 390px wide the
     two would leave it about 130px. Everything stacks: device, then the two
     language groups as wrapped rows, then the note. */
  .rv-wb__stage {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 10px;
  }
  .rv-wb__device {
    grid-column: 1;
    /* First in the stack; it is second in the DOM because the left-hand column
       has to precede it on desktop. */
    order: -1;
  }
  .rv-wb__langs--start,
  .rv-wb__langs--end {
    grid-column: 1;
  }
  /* The language buttons are the one thing here a thumb has to hit, so they are
     sized in px against the 44px touch-target minimum rather than off
     --rv-wb-u, which scales with the DEVICE and had these down to ~20px tall. */
  .rv-wb__langs {
    margin-top: 8px;
  }
  .rv-wb__lang-row {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
  }
  .rv-wb__lang {
    width: auto;
    min-height: 44px;
    padding: 11px 16px;
    font-size: 15px;
  }
  /* International labels only. Four to a row per group is the layout; the Indian
     set already achieves it at the sizes above (its two groups measure 299px and
     282px of buttons in a 358px row at 390px), but the international labels are
     much longer — 352px and 362px — so they wrapped to 3 + 1, giving four ragged
     rows instead of two.

     Both numbers scale with the viewport rather than being fixed, because the
     binding case is not the 390px phone but the narrow end: at 320px a fixed 15px
     with 16px padding needs ~410px. The clamps land at ~14px/8px on a 390px
     screen and bottom out at 12px/6px, which fits the widest group (Arabic,
     Français, Español, Русский) down to 320px with room to spare.

     min-height: 44px above is untouched — that is the touch-target floor, and only
     the horizontal padding and the type shrink. Row gap comes down with them.
     flex-wrap stays: if a future label is longer than these allow, wrapping to a
     third row is a better failure than overflowing a clipped container. */
  .rv-wb--intl .rv-wb__lang-row {
    gap: 8px;
  }
  .rv-wb--intl .rv-wb__lang {
    padding-inline: clamp(6px, 2vw, 12px);
    font-size: clamp(12px, 3.6vw, 15px);
  }
  .rv-wb__lang-note {
    margin-top: 14px;
    /* In px, not off --rv-wb-u, for the same reason the buttons are: on a phone
       that unit tracks the DEVICE and would put this at ~7px. */
    font-size: 16px;
  }
}

/* Reduced motion: the finished composition, no movement. Every reveal above is
   opacity/transform on a class, so neutralising the transitions and forcing the
   end state is enough — nothing is left hidden the way an early `return` in the
   JS would leave it. */
@media (prefers-reduced-motion: reduce) {
  /* The section is its own content at every width now, so this block no longer
     has to undo a pin — it only has to hold everything still. */
  .rv-wb__device,
  .rv-wb__toolbar,
  .rv-wb__tools,
  .rv-wb__sphere,
  .rv-wb__chip,
  .rv-wb__lesson,
  .rv-wb__copy,
  .rv-wb__lang,
  .rv-wb__lang-note {
    transition: none;
  }
  .rv-wb__device,
  .rv-wb__toolbar,
  .rv-wb__tools,
  .rv-wb__sphere,
  .rv-wb__chip,
  .rv-wb__lesson,
  .rv-wb__copy,
  .rv-wb__lang,
  .rv-wb__lang-note {
    opacity: 1;
    transform: none;
  }
  /* Belt and braces. The JS declines to add .is-attracting under this setting, so
     this only matters if a stale cached script does — a pulsing ring is exactly
     the movement being asked for less of. */
  .rv-wb.is-attracting .rv-wb__lang:not([aria-pressed='true']) {
    animation: none;
  }
  .rv-wb__sphere {
    height: 78%;
  }
  .rv-wb__device,
  .rv-wb__toolbar,
  .rv-wb__sphere {
    filter: none;
  }
  .rv-wb__eq-ink,
  .rv-wb.is-s2 .rv-wb__eq-ink {
    animation: none;
    clip-path: inset(0 0 0 0);
  }
  .rv-wb__eq-ink::after {
    display: none;
  }
  .rv-wb__lang-trace rect {
    transition: none;
  }
}

/* ==========================================================================
   Section 5 — Connected Classroom App (sticky intro + stacking card deck)
   ========================================================================== */
.rv-cc {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  --cc-top: 88px; /* where the intro parks (clears the fixed header) */
  /* Where each card parks, below the intro. Capped so the parked card always
     fits above the fold: a card is ~495px tall, so on a short viewport 300px of
     offset left 195px of every card permanently below the fold — which is what
     cut the copy off on a 13" Windows laptop (≈600px of viewport height).
     520px = card height + a small margin. Above ~820px tall the min() picks
     300px and nothing changes on a normal desktop. */
  --cc-stick: min(300px, calc(100vh - 520px));
  --cc-hold: 26vh; /* scroll distance a card holds before the next covers it */
  /* Trailing room so the LAST card can still reach its parked position:
     must exceed (viewport − card height − --cc-stick). */
  --cc-tail: 30vh;
  position: relative;
  background: #f4f8fd; /* shared section tint */
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.rv-cc__inner {
  /* 1248 = 1200 content + the 24px side padding below, so the content
     edge matches every other section. */
  max-width: 1248px;
  margin: 0 auto;
  padding: 0 24px 12vh;
}
/* --- Sticky intro --- */
/* Sticky intro — parks at the top and stays for the whole section.
   NB z-index sits BELOW the cards. Sticky release depends on element height,
   so the (much taller) cards un-stick well before the intro does; if the intro
   painted on top, its opaque band sliced through the card on the way out.
   Cards never rise above their own parked position, so there is no overlap
   until the section exits — at which point they correctly slide over it. */
.rv-cc__intro {
  position: sticky;
  top: var(--cc-top);
  z-index: 1;
  background: #f4f8fd; /* must match .rv-cc */
  width: 100%;
  padding: 26px 0 28px;
  display: flex;
  gap: 40px;
  justify-content: space-between;
  align-items: flex-start;
}
.rv-cc__heading {
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-style: normal;
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
  max-width: 16ch;
  text-wrap: balance;
}
.rv-cc__kicker {
  margin-top: 10px;
  color: var(--rv-navy);
  font-size: var(--rv-fs-lede);
  line-height: 1.5;
}
.rv-cc__copy {
  flex: 0 1 430px;
  color: var(--rv-muted);
  font-size: var(--rv-fs-lede);
  line-height: 1.55;
  padding-top: 6px;
}
/* --- Card deck --- */
.rv-cc__stack {
  position: relative;
  z-index: 2; /* above the intro — see the note on .rv-cc__intro */
  display: flex;
  flex-direction: column;
  gap: var(--cc-hold);
  /* The stack's own box overlaps the intro once scrolled; it is transparent,
     but without this it would swallow selection/clicks on the heading. */
  pointer-events: none;
}
.rv-cc__card {
  pointer-events: auto;
}
/* Trailing room so the LAST card can still reach its parked position and hold.
   This must be a real item, not padding: sticky is constrained by the parent's
   CONTENT box, which padding sits outside of. */
.rv-cc__stack::after {
  content: "";
  flex: 0 0 auto;
  height: var(--cc-tail);
}
/* Each card parks at the same offset, so the next one slides up and covers it
   completely — opaque, no fade (matches the reference). Later cards paint on
   top of earlier ones by DOM order. */
.rv-cc__card {
  position: sticky;
  top: var(--cc-stick);
}
.rv-cc__card-inner {
  display: flex;
  gap: 30px;
  align-items: stretch;
  padding: 26px;
  border-radius: var(--rv-radius-card);
  background: #ffffff;
  box-shadow: 0 18px 44px rgba(20, 40, 77, 0.1);
}
.rv-cc__card-text {
  flex: 1 1 44%;
  display: flex;
  flex-direction: column;
  padding: 6px 6px 14px 10px;
}
/* The shared CCA chip. Sized by the HEIGHT it has to hold, not by a width picked
   for the artwork it replaced: the three per-card strips were 253x63 (aspect 4.02)
   and rendered 190px wide, so 47.3px tall. The chip is 110x45 (aspect 2.44), and
   at 190px it would stand 78px tall and push the card's title and copy down. 116px
   reproduces that 47.3px height exactly, so replacing the artwork moved nothing
   else in the card. Same reasoning behind the two breakpoint widths below — they
   preserve the heights their old widths gave (37.4px and 34.9px).
   Scale all three together if the chip wants to be larger. */
.rv-cc__card-icons {
  width: 116px;
  max-width: 60%;
  height: auto;
  display: block;
}
.rv-cc__card-copy {
  margin-top: auto; /* title + desc sit at the bottom, as in the design */
}
.rv-cc__card-title {
  color: var(--rv-navy);
  font-weight: 500;
  font-style: normal;
  font-size: clamp(1.4rem, 2.4vw, 2rem);
  line-height: 1.2;
}
.rv-cc__card-desc {
  margin-top: 12px;
  max-width: 42ch;
  color: var(--rv-muted);
  font-size: var(--rv-fs-body);
  line-height: 1.55;
}
.rv-cc__card-media {
  flex: 1 1 56%;
  border-radius: var(--rv-radius-media);
  overflow: hidden;
}
.rv-cc__card-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (max-width: 1024px) {
  .rv-cc {
    --cc-stick: 250px;
  }
  .rv-cc__card-inner {
    gap: 20px;
    padding: 20px;
  }
  .rv-cc__card-icons {
    width: 92px;
  }
}

/* Phones: cards still stack, but the intro scrolls away rather than staying
   pinned — a sticky intro plus a full-width card doesn't fit in a phone
   viewport, and the card would slide underneath it. */
@media (max-width: 640px) {
  .rv-cc {
    --cc-stick: 76px;
    --cc-hold: 16vh;
    /* Cards are short relative to a tall phone viewport, so the last one needs
       a lot more trailing room to reach its parked position. */
    --cc-tail: 58vh;
  }
  .rv-cc__inner {
    padding: 0 16px 48px;
  }
  .rv-cc__intro {
    position: static;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
    padding: 40px 0 24px;
  }
  .rv-cc__heading {
    max-width: 100%; /* "Connected Classroom App" fits on one line at 24px */
  }
  .rv-cc__copy {
    flex-basis: auto;
    padding-top: 0;
  }
  /* cards keep position: sticky from the base rule so they stack */
  .rv-cc__card-inner {
    flex-direction: column;
    gap: 18px;
    padding: 16px;
  }
  .rv-cc__card-text {
    padding: 4px 4px 0;
  }
  .rv-cc__card-copy {
    margin-top: 16px;
  }
  .rv-cc__card-icons {
    width: 86px;
  }
  .rv-cc__card-media {
    order: -1; /* image above the copy on phones */
  }
}

/* ==========================================================================
   Section 6 — VisionX (auto-advancing feature list + crossfading screenshot)
   ========================================================================== */
.rv-vx {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  --rv-idle: #9aa9c2;
  --rv-rule: #dbe3ef;
  background: #ffffff;
  padding: 64px 24px 72px;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.rv-vx__inner {
  max-width: 1200px;
  margin: 0 auto;
}
/* --- Brand lockup --- */
/* Head: heading left, sub-copy right — the same flex row used by the agents,
   whiteboard, connected and hardware sections. */
.rv-vx__head {
  display: flex;
  gap: 40px;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 34px;
}
.rv-vx__subtitle {
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-style: normal;
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
  /* 24ch (619px at the 40px desktop size) because the copy is now one line on its
     own: "Device and Admin Management" measures 608px, and at the 22ch this was
     while the lockup sat inline it broke after "Admin". The lockup above it is
     310px wide, so it is never the constraint. */
  max-width: 24ch;
}
/* The VisionX brand lockup, on its own line above the copy.

   Sized against the COPY's cap height rather than at some absolute pixel size, so
   the pair holds its proportions at every breakpoint from one rule. The wordmark
   sets at the SAME cap height as the line beneath it, which makes the two lines
   read as a matched pair:

     the wordmark's caps run y 18.743..60.009 of the 79-unit box, i.e. 0.5224 of it
     so height = 0.727 / 0.5224 = 1.392em, and the width follows the 304:79 aspect

   That leaves the icon at ~1.7x the cap height, which is what gives the mark its
   presence without dominating.

   IT WAS 2.016em, taken from the reference's proportions (icon at ~2.5x the cap).
   That read as oversized once it was on the page rather than in a crop: it made the
   left column 138px tall against a ~50px lede beside it, and the two-column head
   aligns them at the top, so the lede was left floating. Scale it by changing this
   one ratio — the width and the wordmark/copy relationship follow from it.

   display: block is wanted here (the site's base stylesheet already sets it on
   img): the mark is a line of its own, not part of the sentence. That is the
   opposite of what the inline version needed, so do not carry an `inline` over
   from it. */
.rv-vx__logo {
  display: block;
  height: 1.392em;
  width: 5.354em;
  /* Gap to the copy below. Only part of the visible gap: the copy's line box
     contributes ~11px above its caps at the desktop size, so 0.3em of margin
     lands the total at ~23px, which is 0.8x the copy's 29px cap height — the
     proportion the reference reads at. Measured on the render, since the line
     box's share is not something to predict from the numbers. */
  margin: 0 0 0.3em;
}
.rv-vx__lede {
  flex: 0 1 430px;
  color: var(--rv-muted);
  font-size: var(--rv-fs-lede);
  line-height: 1.55;
  padding-top: 6px;
}

/* --- Two-column body --- */

.rv-vx__body {
  display: flex;
  gap: 44px;
  align-items: center;
}
/* No panel styling here — each screenshot asset already ships with its own
   light rounded backdrop and padding, which is also why the column is capped:
   at full width that built-in padding left a lot of dead space beside the list. */
.rv-vx__media {
  flex: 0 1 52%;
  max-width: 540px;
  min-width: 0;
}
/* Screenshots stack and crossfade; the first one holds the height. */
.rv-vx__shots {
  position: relative;
}
.rv-vx__shot {
  width: 100%;
  height: auto;
  display: block;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.rv-vx__shot:not(:first-child) {
  position: absolute;
  inset: 0;
  height: 100%;
  object-fit: contain;
}
.rv-vx__shot.is-active {
  opacity: 1;
}
/* --- Feature list --- */
.rv-vx__list {
  flex: 1 1 44%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.rv-vx__item {
  display: flex;
  gap: 18px;
  align-items: stretch;
  padding: 18px 0;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.rv-vx__item:focus-visible {
  outline: 2px solid #2f6bff;
  outline-offset: 4px;
  border-radius: 6px;
}
/* Left rule; the fill doubles as the auto-advance timer. */
.rv-vx__bar {
  position: relative;
  flex: 0 0 3px;
  border-radius: 3px;
  background: var(--rv-rule);
  overflow: hidden;
}
.rv-vx__bar-fill {
  position: absolute;
  inset: 0;
  background: var(--rv-navy);
  transform: scaleY(0);
  transform-origin: top center;
}
.rv-vx__item-title {
  display: block;
  color: var(--rv-idle);
  font-weight: var(--rv-fw-title);
  font-style: normal;
  font-size: var(--rv-fs-title);
  line-height: 1.3;
  transition: color 0.3s ease;
}
.rv-vx__item.is-active .rv-vx__item-title {
  color: var(--rv-navy);
}
/* Description only shows on the active item */
.rv-vx__item-desc {
  display: block;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  color: var(--rv-muted);
  font-size: var(--rv-fs-body);
  line-height: 1.5;
  transition: max-height 0.4s ease, opacity 0.3s ease, margin-top 0.4s ease;
}
.rv-vx__item.is-active .rv-vx__item-desc {
  max-height: 8em;
  opacity: 1;
  margin-top: 8px;
}

@media (max-width: 1024px) {
  .rv-vx {
    padding: 48px 24px 56px;
  }
  .rv-vx__body {
    gap: 32px;
  }
  .rv-vx__head {
    margin-bottom: 28px;
  }
}
@media (max-width: 860px) {
  .rv-vx__body {
    flex-direction: column;
    align-items: center;
    gap: 24px;
  }
  .rv-vx__media {
    flex: 0 0 auto;
    width: 100%;
    max-width: 560px;
  }
  .rv-vx__list {
    width: 100%;
  }
}
@media (max-width: 640px) {
  .rv-vx {
    padding: 40px 16px 48px;
  }
  .rv-vx__head {
    flex-direction: column;
    align-items: center;
    text-align: center; /* matches the other stacked heads */
    gap: 12px;
    margin-bottom: 22px;
  }
  .rv-vx__subtitle {
    max-width: 24ch;
  }
  /* The head centres at this breakpoint, and text-align does not move a block
     element — the lockup needs its own centring. */
  .rv-vx__logo {
    margin-inline: auto;
  }
  /* Column flex: basis applies to height, so 430px would force a 430px-tall
     paragraph. Same reset the agents/hardware ledes use. */
  .rv-vx__lede {
    flex-basis: auto;
    padding-top: 0;
  }
  .rv-vx__item {
    gap: 14px;
    padding: 10px 0;
  }
}
/* No auto-advance without motion: show every description so nothing is hidden
   behind a timer the user can't perceive. */
@media (prefers-reduced-motion: reduce) {
  .rv-vx__shot {
    transition: none;
  }
  .rv-vx__item-desc {
    max-height: 8em;
    opacity: 1;
    margin-top: 8px;
    transition: none;
  }
  .rv-vx__item-title {
    color: var(--rv-navy);
  }
}

/* ==========================================================================
   Section 7 — Powered by Next-Gen Hardware (bento grid)
   ========================================================================== */
.rv-hw {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  background: #f4f8fd;
  padding: 64px 24px 72px;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.rv-hw__inner {
  max-width: 1200px;
  margin: 0 auto;
}
.rv-hw__head {
  display: flex;
  gap: 40px;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 34px;
}
.rv-hw__heading {
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-style: normal;
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
  max-width: 16ch;
  text-wrap: balance;
}
.rv-hw__intro {
  flex: 0 1 380px;
  color: var(--rv-muted);
  font-size: var(--rv-fs-lede);
  line-height: 1.55;
  padding-top: 6px;
}

/* --- Bento grid: tall | stacked pair | tall --- */
.rv-hw__grid {
  display: grid;
  grid-template-columns: 1fr 1fr 0.94fr;
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: 16px;
  height: clamp(520px, 46vw, 650px);
}
.rv-hw__card--octa {
  grid-column: 1;
  grid-row: 1 / span 2;
}
.rv-hw__card--camera {
  grid-column: 2;
  grid-row: 1;
}
.rv-hw__card--mic {
  grid-column: 2;
  grid-row: 2;
}
.rv-hw__card--npu {
  grid-column: 3;
  grid-row: 1 / span 2;
}

.rv-hw__card {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  position: relative;
  isolation: isolate; /* keep the gradient layer's z-index local to the card */
  /* Radius, face and shadow all match the Connected Classroom cards
     (.rv-cc__card-inner) — same token, same white, same shadow — so the two
     sections read as one card system. The face was #dedede; the grey was the only
     thing that differed.

     The product renders all carry an alpha channel and composite straight onto
     this face. Watch the MIC render if these are ever re-exported: 81% of its
     opaque pixels are near-white (against 1-3% for the other three), so it is the
     one image that depends on the face behind it for its edges. */
  border-radius: var(--rv-radius-card);
  background: #ffffff;
  box-shadow: 0 18px 44px rgba(20, 40, 77, 0.1);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 0.35s ease;
}
/* Hover face. A solid colour cannot be transitioned into a gradient, so the
   gradient lives on its own layer and its opacity is faded instead. Clipped to
   the card radius by the card's overflow: hidden. */
.rv-hw__card::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: 0;
  background: linear-gradient(180deg, #3b75d7 0%, #009eff 100%);
  opacity: 0;
  transition: opacity 0.35s ease;
}
.rv-hw__media {
  position: relative;
  z-index: 1; /* above .rv-hw__card::before */
  flex: 1 1 auto;
  min-height: 0;
}
.rv-hw__media img {
  --img-scale: 1; /* per-card resting scale; hover multiplies it */
  display: block;
  width: 100%;
  height: 100%;
  transform: scale(var(--img-scale));
  transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Touch devices can't hover, so the same highlight follows the scroll instead:
   initHardwareSpotlight() marks whichever card is nearest the middle of the
   viewport as .is-active, and it hands off to the next card as you scroll.
   Gated to (hover: none) so it can never fight the real hover state above.
   Deliberately no lift or image scale here — the card is already moving with
   the scroll, and adding transform on top read as jitter. */
@media (hover: none) {
  .rv-hw__card.is-active {
    box-shadow: 0 28px 60px rgba(20, 40, 77, 0.17);
  }
  .rv-hw__card.is-active::before {
    opacity: 1;
  }
  .rv-hw__card.is-active .rv-hw__label {
    color: #ffffff;
  }
}

/* Hover: the card lifts and the render pushes in slightly (clipped by the
   card's overflow). Only where hover actually exists, so tapping on touch
   devices doesn't leave a card stuck in its hovered state. */
@media (hover: hover) {
  .rv-hw__card:hover {
    transform: translateY(-6px);
    box-shadow: 0 28px 60px rgba(20, 40, 77, 0.17);
  }
  .rv-hw__card:hover .rv-hw__media img {
    transform: scale(calc(var(--img-scale) * 1.05));
  }
  .rv-hw__card:hover::before {
    opacity: 1;
  }
  /* Navy would not hold on the blue gradient */
  .rv-hw__card:hover .rv-hw__label {
    color: #ffffff;
  }
}
.rv-hw__label {
  position: relative;
  z-index: 1; /* above .rv-hw__card::before */
  flex: 0 0 auto;
  padding: 0 26px 26px;
  color: var(--rv-navy);
  transition: color 0.35s ease;
  font-weight: var(--rv-fw-title);
  font-style: normal;
  font-size: var(--rv-fs-title);
  line-height: 1.25;
}

/* The two tall cards bleed their render to the card edges; the two small ones
   keep the product centred with breathing room. */
.rv-hw__card--octa .rv-hw__media img {
  object-fit: cover;
}
/* The NPU render sits in a lot of empty canvas (231px of its 1254px height is
   above the chip), so at its natural size it floated with dead card above and
   below. It is scaled up to fill the card and scaled from the TOP edge, not the
   centre, so the growth goes downward: that keeps clear space under the label and
   lets the chip bleed off the bottom and sides, as in the design.
   Scale is tied to this asset's padding — re-measure if the render changes. */
.rv-hw__card--npu .rv-hw__media img {
  object-fit: cover;
  transform-origin: center top;
  --img-scale: 1.4;
}
.rv-hw__card--camera .rv-hw__media {
  padding: 26px 26px 8px;
}
.rv-hw__card--camera .rv-hw__media img {
  object-fit: contain;
}
/* The mic render carries a lot of empty canvas around a thin bar, so it is
   scaled up (and clipped by the card) to read at the size in the design. */
.rv-hw__card--mic .rv-hw__media {
  padding: 10px 0 0;
}
/* The mic render's transparent margins are asymmetric (74px left vs 56px right),
   so `contain` — which letterboxes the raster inside the card before scaling —
   left a visible gap on the left while the right edge sat flush. `cover` makes
   the raster width track the card width at every breakpoint, so the artwork
   bleeds past both edges regardless of the card's aspect ratio. */
.rv-hw__card--mic .rv-hw__media img {
  object-fit: cover;
  --img-scale: 1.32;
}
/* NPU reads top-down, so its label needs the padding above instead. */
.rv-hw__card--npu .rv-hw__label {
  padding: 26px 26px 0;
}

@media (max-width: 1024px) {
  .rv-hw {
    padding: 48px 24px 56px;
  }
  /* Explicit row heights: with auto rows the cards size to the images'
     intrinsic height and balloon. */
  .rv-hw__grid {
    gap: 14px;
    height: auto;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 280px 280px 320px;
  }
  .rv-hw__card--npu {
    grid-column: 1 / span 2;
    grid-row: 3;
  }
  .rv-hw__label {
    padding: 0 20px 20px;
  }
  .rv-hw__card--npu .rv-hw__label {
    padding: 20px 20px 0;
  }
}
@media (max-width: 640px) {
  .rv-hw {
    padding: 40px 16px 48px;
  }
  .rv-hw__head {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
    margin-bottom: 24px;
  }
  .rv-hw__intro {
    flex-basis: auto;
    padding-top: 0;
  }
  /* One card per row, all the same height. Don't give a card an explicit
     height here — anything taller than the row overflows onto the next card. */
  .rv-hw__grid {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    grid-auto-rows: 300px;
  }
  .rv-hw__card--octa,
  .rv-hw__card--camera,
  .rv-hw__card--mic,
  .rv-hw__card--npu {
    grid-column: 1;
    grid-row: auto;
  }
}

/* Keep the shadow feedback, drop the movement */
@media (prefers-reduced-motion: reduce) {
  .rv-hw__card,
  .rv-hw__media img {
    transition: none;
  }
  .rv-hw__card:hover {
    transform: none;
  }
  .rv-hw__card:hover .rv-hw__media img {
    transform: scale(var(--img-scale));
  }
}

/* ==========================================================================
   Section 8 — Certified by Google (3-up trust badges)
   ========================================================================== */
.rv-gc {
  --rv-navy: #14284d;
  background: #ffffff;
  padding: 64px 24px 72px;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.rv-gc__inner {
  max-width: 1200px;
  margin: 0 auto;
}
.rv-gc__heading {
  /* Capped so the longer copy breaks into two balanced lines instead of one
     very wide single line across the 1200px inner. */
  max-width: 24ch;
  margin: 0 auto 56px;
  text-align: center;
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-style: normal;
  font-size: var(--rv-fs-h2);
  line-height: 1.2;
  text-wrap: balance;
}
.rv-gc__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 40px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.rv-gc__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
/* Fixed box so the three badges sit on a common baseline despite the
   renders having different amounts of built-in padding. */
.rv-gc__icon {
  height: 132px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 26px;
}
.rv-gc__icon img {
  max-height: 100%;
  width: auto;
  display: block;
  object-fit: contain;
}
/* Per-badge optical sizing — the Android head is short and wide, the shield
   tall and narrow, so equal box heights alone don't read as balanced. */
.rv-gc__icon--apps img {
  max-height: 96%;
}
.rv-gc__icon--protect img {
  max-height: 100%;
}
.rv-gc__icon--android img {
  max-height: 112%;
}
/* 19ch and NO balance: that reproduces the design's break points
   ("Built-In Enterprise / Security", "Future-Ready OS / Performance").
   Balancing re-optimises them into different, less natural splits. */
.rv-gc__title {
  max-width: 19ch;
  color: var(--rv-navy);
  font-weight: var(--rv-fw-title);
  font-style: normal;
  font-size: var(--rv-fs-title);
  line-height: 1.3;
}

@media (max-width: 1024px) {
  .rv-gc {
    padding: 48px 24px 56px;
  }
  .rv-gc__heading {
    margin-bottom: 40px;
  }
  .rv-gc__grid {
    gap: 28px;
  }
  .rv-gc__icon {
    height: 108px;
    margin-bottom: 20px;
  }
}
@media (max-width: 640px) {
  .rv-gc {
    padding: 40px 16px 48px;
  }
  .rv-gc__heading {
    margin-bottom: 32px;
  }
  .rv-gc__grid {
    grid-template-columns: 1fr;
    gap: 36px;
  }
  .rv-gc__icon {
    height: 96px;
    margin-bottom: 16px;
  }
}

/* ==========================================================================
   Section 9 — Customer Testimonials (click-to-play video carousel)
   ========================================================================== */
.rv-tm {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  /* Matches the 1200px + 24px container the other sections use, so the first
     card's left edge lines up with every other section's content edge while
     the track itself still scrolls full-bleed. */
  --rv-gutter: max(24px, calc((100% - 1200px) / 2));
  background: #f4f8fd; /* same tint as the hardware section */
  padding: 64px 0 72px;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
/* Same size/weight as the other section headings */
.rv-tm__heading {
  /* 14px, not the 36px the other sections use, because .rv-tm__track adds 28px of
     its own top padding as room for the cards' shadow. 14 + 28 reproduces the
     42px gap this heading had before that padding existed. Change both together. */
  margin: 0 auto 14px;
  padding: 0 24px;
  text-align: center;
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-style: normal;
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
}
.rv-tm__viewport {
  position: relative;
}
/* Native horizontal scroller: snaps, works with touch/trackpad, arrows just
   nudge it by one card. */
.rv-tm__track {
  display: flex;
  gap: 24px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* Block padding is the SHADOW's room, not spacing. The track is a scroll
     container, so it clips at its padding edge, and the cards' shadow reaches
     26px above them (44px blur less the 18px offset) and 62px below. At the old
     6px/26px the shadow was cut off — worst along the bottom, where it is
     strongest. The section's own padding is reduced by the same amounts below, so
     the gap the visitor sees above and below the cards is unchanged.
     The INLINE padding must stay exactly --rv-gutter: it is what lines the first
     card up with every other section's content edge, and scroll-padding-inline
     below has to match it. */
  padding: 28px var(--rv-gutter) 62px;
  /* Without this, snap-align:start snaps cards to the scrollport edge and the
     track silently scrolls past its own left padding, killing the alignment. */
  scroll-padding-inline: var(--rv-gutter);
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.rv-tm__track::-webkit-scrollbar {
  display: none;
}
.rv-tm__card {
  flex: 0 0 min(84vw, 540px);
  scroll-snap-align: start;
}
/* Same card as the Connected Classroom and hardware sections — white face,
   --rv-radius-card, and the one shared shadow — so every card on the page reads
   as the same object.

   This replaced a 2px purple-to-blue GRADIENT BORDER, done with the
   padding-box/border-box background trick. Its comment claimed it matched the
   EduAI agent cards; that stopped being true when the EduAI section was rebuilt,
   so these were the only cards on the page still wearing it. Do not reinstate it
   without changing the other card sections too.

   The border is gone rather than recoloured: a border plus this shadow reads as
   two separate edges on the same card. */
.rv-tm__card-inner {
  height: 100%;
  border-radius: var(--rv-radius-card);
  padding: 14px;
  background: #ffffff;
  box-shadow: 0 18px 44px rgba(20, 40, 77, 0.1);
  /* Same curve and duration as the hardware cards, so the two sections lift with
     one motion rather than two similar ones. */
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 0.35s ease;
}
/* Hover: the card lifts and grows a little, into the deeper of the two shared
   shadows — the same pair of values the hardware cards use.

   Only where hover exists, so a tap on a touch device cannot leave a card stuck
   lifted. There is no scroll-driven stand-in here the way the hardware section
   has one: these cards sit in a horizontal scroller a visitor is already dragging,
   and a card that grew as it passed the middle would fight that gesture.

   The 1.02 is deliberately small. The card is up to 540px wide, so it already
   grows ~11px across at that scale, and everything inside it — the poster, the
   title, the play button — scales with it. Much more and the poster visibly
   softens, since it is a bitmap being drawn above its natural size.

   The growth fits the room the track already reserves for the shadow: 6px of lift
   plus ~4px of half-height growth against 28px of top padding. */
@media (hover: hover) {
  .rv-tm__card-inner:hover {
    transform: translateY(-6px) scale(1.02);
    box-shadow: 0 28px 60px rgba(20, 40, 77, 0.17);
  }
}

/* --- Video facade --- */
.rv-tm__video {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: var(--rv-radius-media);
  overflow: hidden;
  background: #0e1116;
}
.rv-tm__poster,
.rv-tm__video iframe,
.rv-tm__video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  object-fit: cover;
}
/* Stand-in poster until real thumbnails land */
.rv-tm__poster--placeholder {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 10px;
  padding: clamp(18px, 3.4vw, 34px);
  background: linear-gradient(135deg, #1b2430 0%, #0e1116 55%, #131a24 100%);
}
.rv-tm__brand {
  position: absolute;
  top: clamp(14px, 2.4vw, 22px);
  right: clamp(14px, 2.4vw, 22px);
  color: #ffffff;
  font-size: clamp(0.8rem, 1.3vw, 1rem);
  font-weight: 500;
}
.rv-tm__poster-title {
  max-width: 14ch;
  color: #ffffff;
  font-weight: 700;
  font-size: clamp(1.1rem, 2.6vw, 1.85rem);
  line-height: 1.22;
}
.rv-tm__rule {
  width: clamp(70px, 12vw, 130px);
  height: 2px;
  background: rgba(255, 255, 255, 0.85);
}
.rv-tm__winning {
  color: #ffffff;
  font-size: clamp(0.75rem, 1.3vw, 1rem);
  font-weight: 600;
}
.rv-tm__winning b {
  color: #ffb020;
  font-weight: 700;
}
/* Play button */
.rv-tm__play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 68px;
  height: 68px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.94);
  color: #14284d;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
  transition: transform 0.25s ease, background-color 0.25s ease;
}
.rv-tm__play svg {
  width: 30px;
  height: 30px;
  margin-left: 3px;
}
.rv-tm__play:hover {
  transform: translate(-50%, -50%) scale(1.07);
}
.rv-tm__play:focus-visible {
  outline: 3px solid #07acff;
  outline-offset: 3px;
}
.rv-tm__play[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}
.rv-tm__play[disabled]:hover {
  transform: translate(-50%, -50%);
}
/* Once playing, the facade is replaced and the button is gone */
.rv-tm__video.is-playing .rv-tm__poster,
.rv-tm__video.is-playing .rv-tm__play {
  display: none;
}

/* --- Card footer --- */
.rv-tm__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 6px 6px;
}
.rv-tm__person {
  color: var(--rv-navy);
  font-size: clamp(0.95rem, 1.4vw, 1.25rem);
  line-height: 1.35;
}
/* Institution reads first, country beneath it as supporting detail. */
.rv-tm__institution,
.rv-tm__country {
  display: block;
}
.rv-tm__institution {
  font-weight: 600;
}
.rv-tm__country {
  margin-top: 2px;
  color: var(--rv-muted);
  font-size: 0.85em;
}
.rv-tm__logo {
  flex: 0 0 auto;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  object-fit: contain;
}
.rv-tm__logo--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: #eef2f8;
  border: 1px dashed #c3cede;
  color: #8798b3;
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.06em;
}

/* --- Controls: below the track, right-aligned to the content edge --- */
.rv-tm__nav {
  display: flex;
  justify-content: flex-end;
  gap: 14px;
  padding: 4px var(--rv-gutter) 0;
  /* Sits UP inside the track's 62px of bottom padding. That padding is room for
     the cards' shadow, not spacing, so the arrows should ignore it: -36px puts
     them back the 30px below the cards they were before the shadow needed the
     room. Re-derive if the track's bottom padding changes. */
  margin-top: -36px;
}
.rv-tm__arrow {
  width: 52px;
  height: 52px;
  padding: 0;
  border: 1px solid #c9d4e6;
  border-radius: 50%;
  background: transparent;
  color: var(--rv-navy);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color 0.2s ease, border-color 0.2s ease,
    opacity 0.2s ease;
}
.rv-tm__arrow:hover {
  background: #f2f6ff;
  border-color: var(--rv-navy);
}
.rv-tm__arrow:focus-visible {
  outline: 2px solid #07acff;
  outline-offset: 3px;
}
.rv-tm__arrow svg {
  width: 22px;
  height: 22px;
}
/* Dim rather than remove, so the pair doesn't jump around */
.rv-tm__arrow[hidden] {
  display: flex;
  opacity: 0.35;
  pointer-events: none;
}

@media (max-width: 1024px) {
  .rv-tm {
    padding: 48px 0 56px;
  }
  .rv-tm__heading {
    margin-bottom: 30px;
  }
}
@media (max-width: 640px) {
  .rv-tm {
    --rv-gutter: 16px; /* other sections drop to 16px side padding here */
    padding: 40px 0 48px;
  }
  .rv-tm__track {
    gap: 16px;
  }
  .rv-tm__card {
    flex-basis: 86vw;
  }
  .rv-tm__card-inner {
    padding: 10px;
  }
  .rv-tm__play {
    width: 54px;
    height: 54px;
  }
  .rv-tm__play svg {
    width: 24px;
    height: 24px;
  }
  .rv-tm__logo {
    width: 50px;
    height: 50px;
  }
  /* Arrows are below the track now, so they can stay on mobile too */
  .rv-tm__arrow {
    width: 44px;
    height: 44px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .rv-tm__track {
    scroll-behavior: auto;
  }
  .rv-tm__play {
    transition: none;
  }
  /* The card keeps its deeper hover shadow — that is a colour change, not motion —
     but neither lifts nor grows. */
  .rv-tm__card-inner {
    transition: box-shadow 0.35s ease;
  }
  .rv-tm__card-inner:hover {
    transform: none;
  }
}

/* ----- Testimonial video lightbox -----------------------------------------
   Fixed overlay holding a 16:9 dialog. The player itself is injected by JS on
   open and torn out on close, so nothing third-party loads until a visitor
   asks for it and audio genuinely stops when the modal closes. */
.rv-tm__modal[hidden] {
  display: none;
}
.rv-tm__modal {
  position: fixed;
  z-index: 1000;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.rv-tm__modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(9, 18, 35, 0.82);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.rv-tm__modal-dialog {
  position: relative;
  width: min(100%, 1100px);
  aspect-ratio: 16 / 9;
  border-radius: var(--rv-radius-card);
  overflow: hidden;
  background: #000;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
}
.rv-tm__modal-frame,
.rv-tm__modal-frame iframe,
.rv-tm__modal-frame video {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
.rv-tm__modal-close {
  position: absolute;
  z-index: 2;
  top: 10px;
  right: 10px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  cursor: pointer;
  transition: background-color 0.2s ease;
}
.rv-tm__modal-close:hover {
  background: rgba(0, 0, 0, 0.8);
}
.rv-tm__modal-close:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}
.rv-tm__modal-close svg {
  width: 20px;
  height: 20px;
}
/* Set on <body> while the lightbox is open so the page behind cannot scroll */
body.rv-tm-modal-open {
  overflow: hidden;
}

@media (max-width: 640px) {
  .rv-tm__modal {
    padding: 12px;
  }
  .rv-tm__modal-close {
    top: 6px;
    right: 6px;
    width: 34px;
    height: 34px;
  }
}

/* ==========================================================================
   Section 10 — Support beyond software (3-up cards)
   ========================================================================== */
.rv-sp {
  --rv-navy: #14284d;
  --rv-muted: #4c6389;
  background: #ffffff;
  padding: 64px 24px 72px;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.rv-sp__inner {
  max-width: 1200px;
  margin: 0 auto;
}
/* No balance, and 24ch (a `ch` is wider than an average lowercase glyph) to
   reproduce the design's break: "Support beyond software that / only we provide" */
.rv-sp__heading {
  max-width: 24ch;
  margin: 0 auto 44px;
  text-align: center;
  color: var(--rv-navy);
  font-weight: var(--rv-fw-h2);
  font-style: normal;
  font-size: var(--rv-fs-h2);
  line-height: 1.14;
}
.rv-sp__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.rv-sp__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 34px 30px 38px;
  border-radius: var(--rv-radius-card);
  background: #ffffff;
  /* White on white — the shadow is what defines the card */
  box-shadow: 0 10px 34px rgba(20, 40, 77, 0.09);
}
.rv-sp__icon {
  width: 90px;
  height: 90px;
  display: block;
  /* No grayscale filter. It was here because the previous three icons shipped
     in different hues (blue / purple / amber) and needed flattening to read as
     one set. The current set is already a single palette — navy #203A66 on a
     #ECF8FF circle — so desaturating would only grey out the intended design. */
}
.rv-sp__title {
  margin-top: 18px;
  max-width: 18ch;
  color: var(--rv-navy);
  font-weight: var(--rv-fw-title);
  font-style: normal;
  font-size: var(--rv-fs-title);
  line-height: 1.3;
}
.rv-sp__desc {
  margin-top: 12px;
  max-width: 26ch;
  color: var(--rv-muted);
  font-size: var(--rv-fs-body);
  line-height: 1.5;
}

@media (max-width: 1024px) {
  .rv-sp {
    padding: 48px 24px 56px;
  }
  .rv-sp__heading {
    margin-bottom: 32px;
  }
  .rv-sp__grid {
    gap: 18px;
  }
  .rv-sp__card {
    padding: 26px 18px 30px;
  }
  .rv-sp__icon {
    width: 76px;
    height: 76px;
  }
}
@media (max-width: 640px) {
  .rv-sp {
    padding: 40px 16px 48px;
  }
  .rv-sp__heading {
    max-width: 22ch;
    margin-bottom: 26px;
  }
  .rv-sp__grid {
    grid-template-columns: 1fr;
    gap: 16px;
  }
  .rv-sp__card {
    padding: 28px 24px 30px;
  }
}


/* ==========================================================================
   Footer — white (v4.0)

   Replaces the site-wide black footerTMX on THIS PAGE only; see the macro at
   templates/teachmint-v4.0/macros/footer.html for why it is a separate file.
   Namespaced .rv-ft* so nothing here can reach the footer on the other 43
   templates that import the shared one.
   ========================================================================== */
.rv-ft {
  --rv-ft-ink: #1f3965; /* brand navy, as the nav uses */
  --rv-ft-body: #47597a; /* the same secondary tone as the nav dropdowns */
  --rv-ft-rule: rgba(31, 57, 101, 0.14);

  background-color: #ffffff;
  border-top: 1px solid var(--rv-ft-rule);
  color: var(--rv-ft-body);
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}

.rv-ft__inner {
  display: grid;
  /* Company and Resources take a quarter each, Contact the remaining half —
     the proportions the black footer used. */
  grid-template-columns: 1fr 1fr 2fr;
  gap: 40px;
  max-width: 1200px;
  margin: 0 auto;
  padding: 56px 24px 44px;
}

.rv-ft__col-head {
  margin: 0 0 18px;
  font-size: 19px;
  font-weight: 500;
  line-height: 1.2;
  color: var(--rv-ft-ink);
}
/* The heading IS the toggle on phones; on desktop it is only ever a label, so
   the button styling is stripped back to nothing. */
.rv-ft__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: default;
}
.rv-ft__chev {
  display: none; /* phones only */
  width: 12px;
  height: 8px;
  flex: 0 0 auto;
  transition: transform 0.24s ease;
}

.rv-ft__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.rv-ft__list li + li {
  border-top: 1px solid var(--rv-ft-rule);
}
.rv-ft__link {
  display: block;
  padding: 14px 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--rv-ft-body);
  text-decoration: none;
  transition: color 0.18s ease;
}
.rv-ft__link:hover {
  color: var(--rv-ft-ink);
  text-decoration: underline;
}
.rv-ft__link--underline {
  text-decoration: underline;
}

/* Contact: the offices sit two across, as the design shows. */
.rv-ft__contact {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 26px 40px;
}
.rv-ft__office {
  display: flex;
  flex-direction: column;
  font-size: 14px;
  font-style: normal; /* <address> italicises by default */
  line-height: 1.7;
}
.rv-ft__office-city {
  font-weight: 600;
  color: var(--rv-ft-ink);
}
/* Inline inside an address row, so the phone link does not take the full width
   and grow its own hit area across the column. */
.rv-ft__office .rv-ft__link {
  display: inline;
  padding: 0;
}

/* ----- Bottom bar ----- */
.rv-ft__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  max-width: 1200px;
  margin: 0 auto;
  padding: 22px 24px 26px;
  border-top: 1px solid var(--rv-ft-rule);
}
.rv-ft__logo img {
  display: block;
  width: 150px;
  height: auto;
}
.rv-ft__copy {
  margin: 0;
  font-size: 14px;
  color: var(--rv-ft-body);
}
.rv-ft__socials {
  display: flex;
  align-items: center;
  gap: 22px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.rv-ft__socials a {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 44px target on a 21px glyph — WCAG 2.5.8. */
  width: 44px;
  height: 44px;
  margin: -11px; /* keeps the row visually tight despite the larger targets */
  opacity: 0.85;
  transition: opacity 0.18s ease;
}
.rv-ft__socials a:hover {
  opacity: 1;
}
.rv-ft__socials img {
  display: block;
  width: 21px;
  height: auto;
}

.rv-ft :focus-visible {
  outline: 2px solid var(--rv-ft-ink);
  outline-offset: 3px;
  border-radius: 4px;
}

@media (max-width: 1024px) {
  .rv-ft__inner {
    grid-template-columns: 1fr 1fr;
    gap: 32px;
    padding: 44px 20px 32px;
  }
  .rv-ft__col--contact {
    grid-column: 1 / -1;
  }
}

/* Phones: one column, each heading collapses its own block. */
@media (max-width: 640px) {
  .rv-ft__inner {
    grid-template-columns: 1fr;
    gap: 0;
    padding: 28px 20px 8px;
  }
  .rv-ft__col {
    border-bottom: 1px solid var(--rv-ft-rule);
  }
  /* The last column — Contact — drops its rule, because .rv-ft__bar already
     draws one right below it as its own border-top. The two landed ~16px apart
     and read as a double line under "Contact". :last-child rather than naming
     the contact column, so the rule stays "no divider immediately above the
     bar's divider" whichever column ends up last. */
  .rv-ft__col:last-child {
    border-bottom: 0;
  }
  .rv-ft__col-head {
    margin: 0;
    font-size: 17px;
  }
  .rv-ft__toggle {
    /* 48px row: this is the tap target that opens the section. */
    min-height: 48px;
    padding: 12px 0;
    cursor: pointer;
  }
  .rv-ft__chev {
    display: block;
  }
  .rv-ft__toggle[aria-expanded='true'] .rv-ft__chev {
    transform: rotate(180deg);
  }
  /* Collapsed by default here and opened by initRevampFooter. The desktop rules
     above never hide anything, so with scripts blocked every section is open —
     the copy is reachable either way. */
  .rv-ft__list[hidden],
  .rv-ft__contact[hidden] {
    display: none;
  }
  .rv-ft__list {
    padding-bottom: 6px;
  }
  .rv-ft__contact {
    grid-template-columns: 1fr;
    gap: 20px;
    padding-bottom: 18px;
  }
  .rv-ft__bar {
    flex-direction: column;
    gap: 18px;
    padding: 24px 20px 30px;
    text-align: center;
  }
  /* Socials first on a phone, as the black footer had them. */
  .rv-ft__socials {
    order: -1;
    gap: 26px;
  }
}
