/* CMS page stylesheet. Every value is a design-system token: the renderer
   injects :root{--vw-*} per org, so this file never contains a brand value.
   Deliberately NOT Tailwind — frontend/public is outside tailwind.css's
   @source globs, and the admin bundle would blow the CWV budget. */

*,
*::before,
*::after {
  box-sizing: border-box;
}
body.vw-page {
  margin: 0;
  background: var(--vw-color-surface);
  color: var(--vw-color-ink);
  font-family: var(--vw-font-sans);
  font-size: var(--vw-size-base);
  line-height: 1.6;
}

.vw-section {
  padding-block: var(--vw-space-2xl);
}
/* Vertical rhythm, per section. A heading and the grid it introduces are two
   sections, so at that boundary the reader should see no gap at all — only
   the heading section's own bottom padding. "roomy" is the opposite: a real
   change of subject. Both are platform vocabulary, constrained in
   registry.js, so a look cannot invent a fourth. */
.vw-space-joined {
  padding-block-start: 0;
}
.vw-space-roomy {
  padding-block: calc(var(--vw-space-2xl) * 1.6);
}
/* An anchored section jumped to from a menu lands with a little air above
   it instead of its first line kissing the viewport edge. */
.vw-section[id] {
  scroll-margin-top: calc(var(--vw-chrome-height) + var(--vw-space-lg));
}
.vw-section-inner {
  max-width: var(--vw-container-base);
  margin-inline: auto;
  padding-inline: var(--vw-space-lg);
}
.vw-section-inner > * {
  min-width: 0;
}
/* Selectors are keyed by the section KIND the renderer emits verbatim as
   vw-section-<kind> — full-bleed, grid-2/3/4 — never by a shortened form.
   Until 2026-09-03 these read .vw-section-full and .vw-section-grid, which
   nothing ever emitted: full-bleed sections sat inside the container and
   every grid section stacked into one column, on every page, unnoticed
   because no page had used either kind in earnest. */
.vw-section-full-bleed .vw-section-inner {
  max-width: none;
  padding-inline: 0;
}
/* Stacked blocks in a single section share one rhythm. */
.vw-section-single .vw-section-inner > * + * {
  margin-block-start: var(--vw-space-xl);
}
/* A split's halves need room; on the base container each is 472px. */
.vw-section-split .vw-section-inner {
  max-width: var(--vw-container-wide);
}
.vw-section-split .vw-section-inner,
.vw-section-grid-2 .vw-section-inner,
.vw-section-grid-3 .vw-section-inner,
.vw-section-grid-4 .vw-section-inner {
  display: grid;
  gap: var(--vw-space-xl);
}
.vw-section-split .vw-section-inner {
  grid-template-columns: 1fr;
}
.vw-section-grid-2 .vw-section-inner {
  grid-template-columns: 1fr;
}
.vw-section-grid-3 .vw-section-inner {
  grid-template-columns: 1fr;
}
.vw-section-grid-4 .vw-section-inner {
  grid-template-columns: 1fr;
}

.vw-heading {
  font-family: var(--vw-font-display);
  color: var(--vw-color-ink);
  margin: 0 0 var(--vw-space-md);
  line-height: 1.2;
}
/* Optional `title` font slot; unset, large titles stay in `display`. */
.vw-heading-1 {
  font-family: var(--vw-font-title, var(--vw-font-display));
}
.vw-heading-2 {
  font-size: var(--vw-size-3xl);
}
.vw-heading-3 {
  font-size: var(--vw-size-2xl);
}
.vw-heading-4 {
  font-size: var(--vw-size-xl);
}
.vw-richtext-start {
  text-align: start;
}
.vw-text-center {
  text-align: center;
}

.vw-richtext {
  color: var(--vw-color-ink-muted);
}
.vw-richtext p {
  margin: 0 0 var(--vw-space-md);
}
.vw-richtext a {
  color: var(--vw-color-primary);
  /* Accents are decoration, never text (palette.js OPTIONAL_ROLES). */
  text-decoration-color: var(--vw-color-accent, currentColor);
}
.vw-richtext li::marker {
  color: var(--vw-color-accent-2, var(--vw-color-accent, currentColor));
}

.vw-media {
  margin: 0;
}
.vw-media-img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--vw-radius-md);
}
.vw-ratio-square .vw-media-img {
  aspect-ratio: 1 / 1;
  object-fit: cover;
}
.vw-ratio-wide .vw-media-img {
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.vw-ratio-portrait .vw-media-img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
}
/* Video-as-citizen (spec 2026-08-29-website-table-and-video-blocks.md §2b):
   the media block's video branch. Ratio classes above already target
   .vw-media-img only, so mirror the sizing rule here rather than widening
   those selectors — a video never inherits crop-focus (.vw-pos-*) but does
   share the aspect-ratio behavior once a ratio class is on the figure. */
.vw-media-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.vw-ratio-square .vw-media-video {
  aspect-ratio: 1 / 1;
}
.vw-ratio-wide .vw-media-video {
  aspect-ratio: 16 / 9;
}
.vw-ratio-portrait .vw-media-video {
  aspect-ratio: 3 / 4;
}
.vw-caption {
  color: var(--vw-color-muted);
  font-size: var(--vw-size-sm);
  margin-top: var(--vw-space-sm);
}

.vw-gallery {
  display: grid;
  gap: var(--vw-space-md);
  grid-template-columns: 1fr;
}
.vw-gallery-img {
  width: 100%;
  height: auto;
  border-radius: var(--vw-radius-md);
}

.vw-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--vw-space-sm) var(--vw-space-lg);
  border-radius: var(--vw-radius-md);
  text-decoration: none;
  font-weight: 600;
  font-size: var(--vw-size-sm);
}
/* Button VARIANTS are no longer here. They are named catalogue items
   (design_systems.components.buttons) compiled to .vw-btn-<name> rules by
   backend/services/website/catalogueCss.js and emitted inline per org. This
   file keeps only .vw-btn above — the shape every variant shares, including
   display and text-decoration, which the catalogue does not model.
   Adding a button is a design act in the admin screen, not an edit here. */

.vw-quote {
  margin: 0;
  padding-left: var(--vw-space-lg);
  border-left: 3px solid var(--vw-color-accent, var(--vw-color-primary));
}
.vw-quote-text {
  font-family: var(--vw-font-display);
  font-size: var(--vw-size-lg);
  color: var(--vw-color-ink);
  margin: 0 0 var(--vw-space-sm);
}
.vw-quote-cite {
  color: var(--vw-color-muted);
  font-size: var(--vw-size-sm);
  font-style: normal;
}

.vw-divider {
  border: 0;
}
.vw-divider-line {
  border-top: 1px solid var(--vw-color-accent-2, var(--vw-color-border));
  margin-block: var(--vw-space-xl);
}
.vw-divider-space {
  margin-block: var(--vw-space-2xl);
}

.vw-embed {
  position: relative;
  aspect-ratio: 16 / 9;
}
.vw-embed-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: var(--vw-radius-md);
}

.vw-faq-item {
  border-bottom: 1px solid var(--vw-color-border);
  padding-block: var(--vw-space-md);
}
.vw-faq-q {
  font-weight: 600;
  cursor: pointer;
  color: var(--vw-color-ink);
}
.vw-faq-a {
  color: var(--vw-color-ink-muted);
  padding-top: var(--vw-space-sm);
}

.vw-stat {
  display: flex;
  flex-direction: column;
  gap: var(--vw-space-xs);
}
.vw-stat-value {
  font-family: var(--vw-font-display);
  font-size: var(--vw-size-3xl);
  color: var(--vw-color-primary);
}
.vw-stat-label {
  color: var(--vw-color-muted);
  font-size: var(--vw-size-sm);
}

/* Breakpoints are global structure, mirrored in design_systems for analytics
   and the style guide. Media queries cannot read custom properties, so these
   literals are the source of truth and the stored values must equal them. */
@media (min-width: 48rem) {
  .vw-section-split .vw-section-inner {
    grid-template-columns: 1fr 1fr;
  }
  .vw-section-grid-2 .vw-section-inner {
    grid-template-columns: repeat(2, 1fr);
  }
  .vw-section-grid-3 .vw-section-inner {
    grid-template-columns: repeat(3, 1fr);
  }
  .vw-section-grid-4 .vw-section-inner {
    grid-template-columns: repeat(2, 1fr);
  }
  .vw-gallery {
    grid-template-columns: repeat(3, 1fr);
  }
  /* The block emits vw-gallery-<columns>; honour 2 and 4, not only the 3
     the base rule assumes (a 4-column gallery used to wrap 3 + 1). */
  .vw-gallery-2 {
    grid-template-columns: repeat(2, 1fr);
  }
  .vw-gallery-4 {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 64rem) {
  .vw-section-grid-4 .vw-section-inner {
    grid-template-columns: repeat(4, 1fr);
  }
  .vw-gallery-4 {
    grid-template-columns: repeat(4, 1fr);
  }
}
/* Scoped to the CMS page's own content, not the document. This sheet is now
   co-loaded onto static pages for their header and footer, and unscoped it
   reached into their bodies: measured 2026-09-22, it stopped eventlist's
   loading spinner and its tooltip fades for every reduced-motion visitor.
   Nothing is lost by scoping — the chrome's own transitions already live
   inside `prefers-reduced-motion: no-preference` and never apply here. */
@media (prefers-reduced-motion: reduce) {
  body.vw-page,
  body.vw-page *,
  body.vw-page *::before,
  body.vw-page *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------------------------------------------------------- CHROME

   The header, navigation and footer a page renders inside. Emitted by
   backend/services/website/chrome.js, which is pure and knows only class
   names — every value here is a token, same rule as the rest of this file.

   A chrome carries NO layout choices of its own: the catalogue models the
   logo slot, the action button and the footer's legal line, and nothing else.
   Anything that would need a per-chrome rule belongs in the catalogue as a
   named item, not as a class nobody can see. */

:root {
  /* The rendered header's height, kept as a token so
     .vw-section[id]'s scroll-margin-top can add it in rather than guess it. */
  --vw-chrome-height: 4rem;
}

/* The chrome is a full-bleed BAND wrapping a centred row — the same shape as
   .vw-section / .vw-section-inner. The band owns the background so a footer
   surface and the sticky header's fill reach both edges; the row inside keeps
   the content within the wide container. These were one element until
   2026-09-06, so a footer surface painted only the middle 80rem and the header
   and footer sat visibly inset from the full-bleed sections between them. */
.vw-chrome-band {
  width: 100%;
}
/* A textured surface paints an ::after overlay across the band; lift the row
   above it (as .vw-bg-<key> > .vw-section-inner is) or it washes the logo. */
.vw-chrome-band > * {
  position: relative;
  z-index: 1;
}
.vw-chrome-band-footer {
  border-top: 1px solid var(--vw-color-border);
  margin-block-start: var(--vw-space-2xl);
}

.vw-chrome-header {
  display: flex;
  align-items: center;
  gap: var(--vw-space-lg);
  flex-wrap: wrap;
  max-width: var(--vw-container-wide);
  margin-inline: auto;
  padding: var(--vw-space-md) var(--vw-space-lg);
}
/* Opt-in via chromes.<key>.header.sticky (spec §4). On the BAND, so the
   translucent fill reaches both edges of the viewport. */
.vw-chrome-band-sticky {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--vw-color-surface) 88%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.vw-chrome-logo,
/* The big lockup a collapsing header shows at the top of the page. Its
   colour reset lives HERE, at one-class specificity, so a surface's own logo
   ink still wins: emitted alongside the tall rules it came after them and
   painted the big logo in the bar's ink while the inline one was the brand's
   — the same mark in two colours depending on scroll position. */
.vw-chrome-tall-logo {
  display: inline-flex;
  flex-shrink: 0;
  color: inherit; /* or the UA paints this link — and the lockup — blue */
}
.vw-chrome-logo img,
.vw-chrome-logo svg {
  /* Height-bounded, aspect preserved: an unbounded logo is the classic broken
     header. An inlined SVG inherits its colour, so a painted header bar
     (chromes.header.surface) recolours the lockup. */
  height: 2.5rem;
  width: auto;
  display: block;
  color: inherit;
}
.vw-chrome-nav {
  flex: 1 1 100%;
  order: 3;
}
.vw-chrome-action {
  margin-inline-start: auto;
}

.vw-nav,
.vw-nav-sub,
.vw-footer-links {
  list-style: none;
  margin: 0;
  padding: 0;
}
.vw-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vw-space-md) var(--vw-space-lg);
  align-items: center;
}
/* A top-level item is a column, not a row: an item with children stacks its
   label above its sub-list. Items with no children collapse to just the
   link, so this costs nothing for the common case. */
.vw-nav-item {
  display: flex;
  flex-direction: column;
  gap: var(--vw-space-xs);
}
.vw-nav-link,
.vw-nav-label {
  font-size: var(--vw-size-sm);
  font-weight: 500;
  color: var(--vw-color-ink);
  text-decoration: none;
}
.vw-nav-link:hover {
  color: var(--vw-color-primary);
}
/* On a painted bar the brand colour is not guaranteed to read against it, so
   hover dims the bar's own ink instead of swapping colour. */
[class*="vw-bg-"] .vw-chrome-header .vw-nav-link:hover {
  color: inherit;
  opacity: 0.75;
}
/* A label is a dropdown heading, not a link. It reads as one visually and is
   deliberately not focusable — an anchor to nowhere is a keyboard trap. */
.vw-nav-label {
  color: var(--vw-color-ink-muted);
}
/* One level of nesting, and it is a plain indented list rather than a
   hover-revealed dropdown: a hover menu is unusable on touch and needs
   JavaScript to be usable with a keyboard, and this stylesheet ships no
   JavaScript. */
.vw-nav-sub {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vw-space-md);
  padding-inline-start: var(--vw-space-md);
}
.vw-nav-sub .vw-nav-link {
  color: var(--vw-color-ink-muted);
  font-size: var(--vw-size-xs);
}

.vw-chrome-footer {
  display: grid;
  gap: var(--vw-space-xl);
  max-width: var(--vw-container-wide);
  margin-inline: auto;
  padding: var(--vw-space-2xl) var(--vw-space-lg);
}
.vw-footer-col {
  display: grid;
  gap: var(--vw-space-sm);
  align-content: start;
}
.vw-footer-heading {
  font-family: var(--vw-font-display);
  font-size: var(--vw-size-base);
  color: var(--vw-color-ink);
  margin: 0 0 var(--vw-space-sm);
}
.vw-footer-links {
  display: grid;
  gap: var(--vw-space-sm);
}
.vw-footer-links .vw-nav-link {
  color: var(--vw-color-ink-muted);
}
.vw-footer-legal {
  font-size: var(--vw-size-xs);
  color: var(--vw-color-muted);
  margin: 0;
}
/* The footer's own lockup, larger than the header's — a footer signs off,
   it does not navigate. No `color` of its own: an inlined SVG here takes the
   footer's colour, which a surface overrides for free. */
.vw-chrome-footer-logo {
  display: inline-flex;
  margin-bottom: var(--vw-space-md);
  color: inherit; /* hands the inlined SVG the footer's (or surface's) ink */
}
.vw-chrome-footer-logo svg,
.vw-chrome-footer-logo img {
  height: 3rem;
  width: auto;
}

@media (min-width: 48rem) {
  .vw-chrome-nav {
    flex: 1 1 auto;
    order: 0;
  }
  .vw-chrome-nav .vw-nav {
    justify-content: center;
  }
  .vw-chrome-footer {
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  }
  .vw-footer-legal {
    grid-column: 1 / -1;
  }
}

/* Visually hidden, still announced by screen readers and still an h1 for
   crawlers. Used only by the renderer's fallback page-title heading — see
   renderer.js. Not a general utility: authored content names catalogue
   items, never raw classes. */
.vw-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- MOTION

   The five patterns (spec docs/superpowers/specs/2026-08-29-website-motion.md
   §4a). Platform vocabulary, not per-org: a section opts in by carrying
   `vw-motion-<name>` (renderer.js, beside vw-bg); WHETHER a pattern is
   available to an org's authors is a catalogue question (components.motions),
   answered elsewhere — this file only defines what each pattern looks like.

   Two rules, both load-bearing and both pinned by
   backend/services/website/__tests__/motionCss.test.js, not left to review:

   1. EVERYTHING here — including the pre-animation (hidden/offset) states —
      lives inside `@media (prefers-reduced-motion: no-preference)`. A
      visitor whose device asks for reduced motion never sees a hidden or
      offset element: the media query simply does not apply, so nothing here
      touches opacity or transform, and the page renders in its finished,
      static layout.
   2. Every pre-animation (hidden/offset) state is ADDITIONALLY gated on
      `html.vw-motion`, a class ONLY frontend/public/js/vw-motion.js sets, and
      only once it has confirmed the visitor did not ask for reduced motion.
      With the script blocked, failed, or never loaded, `html.vw-motion` never
      appears, the `:not(.vw-in)` selectors below never match, and the page
      renders complete — motion is added on top of a whole page, never a
      precondition for one.

   `drift`/`pin`/`crossfade` do not need that second gate: they never hide
   content behind a literal `opacity: 0`. Each reads a scroll-progress custom
   property the script writes (`--vw-drift`, `--vw-pin-progress`,
   `--vw-fade-progress`) through a `var(--x, <safe-default>)` fallback whose
   default is itself a complete, still picture — undriven, `drift` sits at
   rest, `pin`'s steps stay fully visible, and `crossfade` simply shows its
   first image. No JS, no motion, still nothing missing. */
@media (prefers-reduced-motion: no-preference) {
  /* A menu link to `#anchor` glides rather than snaps. Motion, so it lives
     inside this gate like everything else that moves. */
  html {
    scroll-behavior: smooth;
  }

  /* RISE — the row rises and fades in. The row, not the section: moving the
     section moved its background and showed page ground above it. */
  .vw-motion-rise .vw-section-inner {
    transition:
      opacity 600ms ease-out,
      transform 600ms ease-out;
  }
  html.vw-motion .vw-motion-rise:not(.vw-in) .vw-section-inner {
    opacity: 0;
    transform: translateY(12px);
  }

  /* STAGGER — the section's direct children arrive a beat apart. The
     section itself carries .vw-in (same IntersectionObserver as rise, see
     vw-motion.js); children read it off their ancestor. */
  .vw-motion-stagger .vw-section-inner > * {
    transition:
      opacity 600ms ease-out,
      transform 600ms ease-out;
  }
  html.vw-motion .vw-motion-stagger:not(.vw-in) .vw-section-inner > * {
    opacity: 0;
    transform: translateY(12px);
  }
  /* ~90ms increments, capped at the 8th child — a longer list arrives with
     the last rather than trailing off into a visibly-staggered eternity. */
  .vw-motion-stagger .vw-section-inner > *:nth-child(1) {
    transition-delay: 0ms;
  }
  .vw-motion-stagger .vw-section-inner > *:nth-child(2) {
    transition-delay: 90ms;
  }
  .vw-motion-stagger .vw-section-inner > *:nth-child(3) {
    transition-delay: 180ms;
  }
  .vw-motion-stagger .vw-section-inner > *:nth-child(4) {
    transition-delay: 270ms;
  }
  .vw-motion-stagger .vw-section-inner > *:nth-child(5) {
    transition-delay: 360ms;
  }
  .vw-motion-stagger .vw-section-inner > *:nth-child(6) {
    transition-delay: 450ms;
  }
  .vw-motion-stagger .vw-section-inner > *:nth-child(7) {
    transition-delay: 540ms;
  }
  .vw-motion-stagger .vw-section-inner > *:nth-child(n + 8) {
    transition-delay: 630ms;
  }

  /* DRIFT — a full-bleed image drifts a few percent of its travel. The
     custom property lives on the section (vw-motion.js writes it there) and
     is read by the descendant image, so an undriven section (no JS) sits at
     its default 0px offset: present, still, correct. */
  .vw-motion-drift {
    --vw-drift: 0px;
  }
  .vw-motion-drift .vw-media-img {
    transform: translateY(var(--vw-drift));
    will-change: transform;
  }

  /* PIN — one story moment. The section's content pins in place while the
     section itself scrolls through a taller-than-viewport track, and its
     labelled steps fade up as the script advances --vw-pin-progress. The
     fallback (1 = fully visible) is deliberate: without a script every step
     is simply shown at once, not hidden. */
  .vw-motion-pin {
    position: relative;
    min-height: 180vh;
  }
  .vw-motion-pin .vw-section-inner {
    position: sticky;
    top: var(--vw-space-xl);
  }
  .vw-motion-pin [data-pin-step] {
    opacity: var(--vw-pin-progress, 1);
    transition: opacity 200ms linear;
  }

  /* CROSSFADE — the other story moment. Two stacked full-bleed media figures
     overlay within the section: the first stays in normal flow (it sets the
     section's height), the second sits absolutely over it and fades in as
     --vw-fade-progress advances. The fallback (0 = fully transparent) is
     deliberate too: without a script the section shows exactly its first
     image, a complete and ordinary picture, never a blank overlay. */
  .vw-motion-crossfade .vw-section-inner {
    position: relative;
  }
  .vw-motion-crossfade .vw-media:first-of-type {
    position: relative;
  }
  .vw-motion-crossfade .vw-media:nth-of-type(2) {
    position: absolute;
    inset: 0;
    opacity: var(--vw-fade-progress, 0);
    transition: opacity 200ms linear;
  }

  /* GEOMETRY — the seed-of-life circles (brand kit plan, 2026-09-06). DRAW
     strokes each circle in on enter, like RISE; SCROLL builds the circles up
     one at a time as a PIN section's --vw-pin-progress advances. 377 is the
     circumference of the block's r=60 circle (2 * pi * 60), so a fully
     drawn stroke has offset 0. */
  .vw-geometry-draw .vw-geo-c {
    stroke-dasharray: 377;
    transition: stroke-dashoffset 900ms ease-out;
  }
  html.vw-motion .vw-geometry-draw:not(.vw-in) .vw-geo-c {
    stroke-dashoffset: 377;
    transition-duration: 0ms;
    transition-delay: 0ms;
  }
  .vw-geometry-draw .vw-geo-c:nth-child(2) {
    transition-delay: 180ms;
  }
  .vw-geometry-draw .vw-geo-c:nth-child(3) {
    transition-delay: 360ms;
  }
  .vw-geometry-draw .vw-geo-c:nth-child(4) {
    transition-delay: 540ms;
  }
  .vw-geometry-draw .vw-geo-c:nth-child(5) {
    transition-delay: 720ms;
  }
  .vw-geometry-draw .vw-geo-c:nth-child(6) {
    transition-delay: 900ms;
  }
  .vw-geometry-draw .vw-geo-c:nth-child(7) {
    transition-delay: 1080ms;
  }
  .vw-motion-pin .vw-geometry-scroll .vw-geo-c:nth-child(1) {
    opacity: clamp(0, calc(var(--vw-pin-progress, 1) * 7), 1);
  }
  .vw-motion-pin .vw-geometry-scroll .vw-geo-c:nth-child(2) {
    opacity: clamp(0, calc(var(--vw-pin-progress, 1) * 7 - 1), 1);
  }
  .vw-motion-pin .vw-geometry-scroll .vw-geo-c:nth-child(3) {
    opacity: clamp(0, calc(var(--vw-pin-progress, 1) * 7 - 2), 1);
  }
  .vw-motion-pin .vw-geometry-scroll .vw-geo-c:nth-child(4) {
    opacity: clamp(0, calc(var(--vw-pin-progress, 1) * 7 - 3), 1);
  }
  .vw-motion-pin .vw-geometry-scroll .vw-geo-c:nth-child(5) {
    opacity: clamp(0, calc(var(--vw-pin-progress, 1) * 7 - 4), 1);
  }
  .vw-motion-pin .vw-geometry-scroll .vw-geo-c:nth-child(6) {
    opacity: clamp(0, calc(var(--vw-pin-progress, 1) * 7 - 5), 1);
  }
  .vw-motion-pin .vw-geometry-scroll .vw-geo-c:nth-child(7) {
    opacity: clamp(0, calc(var(--vw-pin-progress, 1) * 7 - 6), 1);
  }
}

/* ----------------------------------------------------------------- TABLE

   spec docs/superpowers/specs/2026-08-29-website-table-and-video-blocks.md
   §1. Outside the reduced-motion block above, on purpose — a table has no
   motion of its own; its only rail-3 concern is scrolling, not animating. */
.vw-table-wrap {
  overflow-x: auto;
}
.vw-table {
  width: 100%;
  border-collapse: collapse;
}
.vw-table caption {
  text-align: left;
  padding-block-end: var(--vw-space-sm);
}
.vw-table th,
.vw-table td {
  padding: var(--vw-space-sm) var(--vw-space-md);
  border: 1px solid var(--vw-color-border);
  text-align: left;
}
.vw-table thead th {
  background: var(--vw-color-surface-raised);
}
.vw-table-em {
  background: var(--vw-color-surface-raised);
  border-top: 2px solid var(--vw-color-accent, var(--vw-color-primary));
}

/* ---- event blocks (spec 2026-09-03) ---- */

/* HERO — text over a full-bleed photo. .vw-hero-media is an absolutely
   positioned backdrop; treatments (.vw-treat-<key>, generated per-org in
   catalogueCss.js) are bare class selectors with no `.vw-media` scoping, so
   a treatment class applied to a `.vw-hero-media` figure already works with
   no selector change here — nothing to extend, nothing to duplicate. This
   block only sizes the image/video inside that figure the way `.vw-media`
   does for its own children, since a hero figure never carries `.vw-media`
   itself. */
.vw-hero {
  position: relative;
  display: grid;
  align-items: center;
  justify-items: center;
}
/* A hero title set as artwork. The <h1> keeps its heading role; only its
   contents become an image, so the type scale no longer applies and the width
   is capped against the hero body instead.

   The cap holds for a raster wordmark, which carries its own pixel size. An
   SVG is stored with no width/height (media-library.routes.js) and svgAsset.js
   strips them from the root element, so it has a ratio but no intrinsic size
   and WILL be drawn at the cap whatever it was drawn at. Giving SVGs real
   dimensions at upload is the fix for that, and it belongs upstream — every
   SVG consumer has the same gap, not just this one. */
.vw-hero-title-media img {
  --vw-hero-wordmark-max: 34rem;
  display: block;
  max-width: min(100%, var(--vw-hero-wordmark-max));
  width: auto;
  height: auto;
}
.vw-hero-center .vw-hero-title-media img {
  margin-inline: auto;
}

.vw-hero-full {
  min-height: 100svh;
}
.vw-hero-tall {
  min-height: 70svh;
}
.vw-hero-short {
  min-height: 45svh;
}
/* Two classes deep on purpose: a treatment class on the same figure sets
   `position: relative` at single-class specificity (catalogueCss.js), and
   an in-flow figure pushes the hero body below the photo. */
.vw-hero .vw-hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  margin: 0;
}
/* event.js emits vw-hero-img / vw-hero-video on the hero's own media; the
   media-block classes are matched too so a treatment host behaves alike. */
.vw-hero-media .vw-hero-img,
.vw-hero-media .vw-hero-video,
.vw-hero-media .vw-media-img,
.vw-hero-media .vw-media-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.vw-hero-body {
  position: relative;
  z-index: 1;
  max-width: var(--vw-container-narrow);
  padding: var(--vw-space-xl) var(--vw-space-lg);
  color: var(--vw-color-surface);
}
.vw-hero-body .vw-heading {
  color: var(--vw-color-surface);
  font-family: var(--vw-font-title, var(--vw-font-display));
  font-size: clamp(2.25rem, 6vw, 4.75rem);
}
.vw-hero-body p {
  color: var(--vw-color-surface);
}
.vw-hero-center {
  text-align: center;
  justify-items: center;
}
.vw-hero-start {
  text-align: start;
  justify-items: start;
}
.vw-hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vw-space-md);
  margin-top: var(--vw-space-lg);
}
.vw-hero-center .vw-hero-actions {
  justify-content: center;
}
/* A hero owns the viewport in a full-bleed section: the section's own
   padding-block would otherwise leave a band of background above/below it.
   `:has` is fine (baseline 2023); a non-`:has` browser gets the padded
   version, still correct, just less flush. */
.vw-section-full-bleed:has(> .vw-section-inner > .vw-hero) {
  padding-block: 0;
}

/* INTRO — the centred eyebrow/title/lead pattern, width-capped so a lead
   never runs the full page width. */
.vw-intro {
  max-width: var(--vw-container-narrow);
  margin-inline: auto;
}
.vw-intro-center {
  text-align: center;
  margin-inline: auto;
}
.vw-intro-start {
  text-align: start;
  margin-inline: 0;
}

/* CARD — a feature or a link, on the raised surface. */
.vw-card {
  display: block;
  background: var(--vw-color-surface-raised);
  color: var(--vw-color-ink);
  border-radius: var(--vw-radius-lg);
  padding: var(--vw-space-lg);
  box-shadow: var(--vw-effect-elevation1);
  text-decoration: none;
}
.vw-card-media {
  aspect-ratio: 4 / 3;
  width: 100%;
  margin: 0;
  overflow: hidden;
  border-radius: var(--vw-radius-md);
  display: block;
}
.vw-card-media img,
.vw-person-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* CARD, TILE LAYOUT — the photo is the card's ground, the text lies over its
   foot. Scrim is a ::before on the tile and the ink is !important so both
   survive a page-local style; see the `layout` prop in blocks/event.js for
   why that matters. */
.vw-card-tile {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: 20rem;
  padding: 0;
  overflow: hidden;
  background: none;
  box-shadow: none;
  border: 1px solid var(--vw-color-border);
  border-radius: var(--vw-radius-md);
}
.vw-card-tile .vw-card-media {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
  border-radius: 0;
  z-index: 0;
}
.vw-card-tile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0.28) 45%, transparent);
}
.vw-card-tile .vw-card-body {
  position: relative;
  z-index: 2;
  width: 100%;
  padding: var(--vw-space-lg);
}
.vw-card-tile .vw-card-title,
.vw-card-tile .vw-card-eyebrow,
.vw-card-tile .vw-card-text {
  color: #fff !important;
}

@media (prefers-reduced-motion: no-preference) {
  a.vw-card {
    transition:
      box-shadow 200ms ease-out,
      transform 200ms ease-out;
  }
  a.vw-card:hover {
    box-shadow: var(--vw-effect-elevation2);
    transform: translateY(-2px);
  }
}

/* CHIPS — a wrapping row of short labels. Colour comes from the section's
   own ink via currentColor, so one block reads correctly on a light ground
   and a dark one without a variant per surface. .vw-chip is deliberately
   absent from catalogueCss.js's TEXT_BEARING list for the same reason: it
   sets no colour of its own, so it simply inherits the surface's. */
.vw-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vw-space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}
.vw-chips-center {
  justify-content: center;
}
.vw-chips-start {
  justify-content: flex-start;
}
.vw-chip {
  padding: 0.4em 0.9em;
  border: 1px solid color-mix(in srgb, currentColor 40%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 7%, transparent);
  font-size: var(--vw-size-sm);
  line-height: 1.3;
}

/* PERSON — a speaker, artist, facilitator. */
.vw-person-photo {
  aspect-ratio: 1 / 1;
  width: 100%;
  margin: 0;
  overflow: hidden;
  display: block;
}
.vw-person-circle .vw-person-photo {
  border-radius: var(--vw-radius-full);
}
.vw-person-square .vw-person-photo {
  border-radius: var(--vw-radius-md);
}
/* No library portrait yet: an initials avatar stands in, same footprint. */
.vw-person-avatar {
  aspect-ratio: 1 / 1;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Role tokens only — a scale name like accent-200 exists for one palette
     and vanishes with the next, taking the avatar's background with it. */
  background: var(--vw-color-surface-raised);
  border: 1px solid var(--vw-color-border);
  color: var(--vw-color-ink);
  font-family: var(--vw-font-display);
  font-size: var(--vw-size-2xl);
}
.vw-person-circle .vw-person-avatar {
  border-radius: var(--vw-radius-full);
}
.vw-person-square .vw-person-avatar {
  border-radius: var(--vw-radius-md);
}
.vw-person-name {
  font-family: var(--vw-font-display);
  color: var(--vw-color-ink);
  margin: var(--vw-space-sm) 0 0;
}
.vw-person-role {
  color: var(--vw-color-muted);
  font-size: var(--vw-size-sm);
}

/* PRICING — ticket tiers. */
.vw-pricing {
  display: grid;
  gap: var(--vw-space-lg);
  grid-template-columns: 1fr;
}
.vw-pricing-tier {
  background: var(--vw-color-surface-raised);
  color: var(--vw-color-ink);
  border-radius: var(--vw-radius-lg);
  padding: var(--vw-space-lg);
  box-shadow: var(--vw-effect-elevation1);
}
.vw-pricing-tier-highlighted {
  background: var(--vw-color-primary);
  color: var(--vw-color-on-primary);
}
.vw-pricing-tier-highlighted .vw-heading,
.vw-pricing-tier-highlighted ul,
.vw-pricing-tier-highlighted li,
.vw-pricing-tier-highlighted p {
  color: var(--vw-color-on-primary);
}
@media (min-width: 48rem) {
  .vw-pricing-2 {
    grid-template-columns: repeat(2, 1fr);
  }
  .vw-pricing-3 {
    grid-template-columns: repeat(3, 1fr);
  }
  .vw-pricing-4 {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 64rem) {
  .vw-pricing-4 {
    grid-template-columns: repeat(4, 1fr);
  }
  .vw-pricing-tier-highlighted {
    transform: scale(1.03);
  }
}

/* COUNTDOWN — to the opening. Tiles are complete without JS: the server
   renders `—` and a plain-text date line; vw-countdown.js fills them in. */
.vw-countdown {
  text-align: center;
}
.vw-countdown-label {
  margin: 0 0 var(--vw-space-md);
}
/* The row is the tiles wrapper, not the block: the label and the date line
   are block-level siblings above and below it. */
.vw-countdown-tiles {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--vw-space-md);
}
.vw-countdown-tile {
  min-width: 5.5rem;
  background: var(--vw-color-surface-raised);
  border-radius: var(--vw-radius-lg);
  box-shadow: var(--vw-effect-elevation1);
  padding: var(--vw-space-md) var(--vw-space-lg);
  text-align: center;
  color: var(--vw-color-ink);
}
.vw-countdown-unit {
  display: block;
  color: var(--vw-color-muted);
  font-size: var(--vw-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.vw-countdown-num {
  display: block;
  font-family: var(--vw-font-display);
  font-size: var(--vw-size-3xl);
  color: var(--vw-color-ink);
}
.vw-countdown-date {
  color: var(--vw-color-muted);
  font-size: var(--vw-size-sm);
  margin-top: var(--vw-space-sm);
}

/* SCHEDULE — the programme. Radio-tab pattern, no script. The markup
   (blocks/event.js) is: inside .vw-schedule-tabs, N hidden radio inputs
   first, then a .vw-schedule-tab-labels row of N labels, then N
   .vw-schedule-day panels carrying data-day-index="0..N-1". Selectors key
   on the input's nth-of-type and the panel's data-day-index — never on a
   div count, because the labels row is itself a div and would shift it.
   A single-day schedule emits no tabs and no panels: just the list. */
.vw-schedule-tabs input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.vw-schedule-tab-labels {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vw-space-sm);
  margin-bottom: var(--vw-space-lg);
}
.vw-schedule-tab-labels label {
  display: inline-block;
  padding: var(--vw-space-sm) var(--vw-space-lg);
  border-radius: var(--vw-radius-full);
  background: var(--vw-color-surface-raised);
  color: var(--vw-color-ink-muted);
  cursor: pointer;
  font-size: var(--vw-size-sm);
  font-weight: 600;
}
.vw-schedule-tabs input:nth-of-type(1):checked ~ .vw-schedule-tab-labels label:nth-of-type(1),
.vw-schedule-tabs input:nth-of-type(2):checked ~ .vw-schedule-tab-labels label:nth-of-type(2),
.vw-schedule-tabs input:nth-of-type(3):checked ~ .vw-schedule-tab-labels label:nth-of-type(3),
.vw-schedule-tabs input:nth-of-type(4):checked ~ .vw-schedule-tab-labels label:nth-of-type(4),
.vw-schedule-tabs input:nth-of-type(5):checked ~ .vw-schedule-tab-labels label:nth-of-type(5),
.vw-schedule-tabs input:nth-of-type(6):checked ~ .vw-schedule-tab-labels label:nth-of-type(6),
.vw-schedule-tabs input:nth-of-type(7):checked ~ .vw-schedule-tab-labels label:nth-of-type(7) {
  background: var(--vw-color-primary);
  color: var(--vw-color-on-primary);
}
.vw-schedule-day {
  display: none;
}
.vw-schedule-tabs input:nth-of-type(1):checked ~ .vw-schedule-day[data-day-index="0"],
.vw-schedule-tabs input:nth-of-type(2):checked ~ .vw-schedule-day[data-day-index="1"],
.vw-schedule-tabs input:nth-of-type(3):checked ~ .vw-schedule-day[data-day-index="2"],
.vw-schedule-tabs input:nth-of-type(4):checked ~ .vw-schedule-day[data-day-index="3"],
.vw-schedule-tabs input:nth-of-type(5):checked ~ .vw-schedule-day[data-day-index="4"],
.vw-schedule-tabs input:nth-of-type(6):checked ~ .vw-schedule-day[data-day-index="5"],
.vw-schedule-tabs input:nth-of-type(7):checked ~ .vw-schedule-day[data-day-index="6"] {
  display: block;
}
.vw-schedule-item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--vw-space-md);
  padding-block: var(--vw-space-md);
  border-bottom: 1px solid var(--vw-color-border);
}
.vw-schedule-time {
  color: var(--vw-color-muted);
  font-size: var(--vw-size-sm);
  white-space: nowrap;
}
.vw-schedule-title {
  font-weight: 600;
  color: var(--vw-color-ink);
}
.vw-schedule-description {
  color: var(--vw-color-ink-muted);
}
.vw-schedule-place {
  color: var(--vw-color-muted);
  font-size: var(--vw-size-sm);
}

/* LOGOS — partners, sponsors, collaborators. Not photos, so the treatment
   system does not apply; grayscale-to-colour is its own rule, gated for the
   transition only. */
.vw-logos {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--vw-space-xl);
}
.vw-logos img {
  width: auto;
  filter: grayscale(1);
}
.vw-logos-sm img {
  height: 2rem;
}
.vw-logos-md img {
  height: 3rem;
}
.vw-logos-lg img {
  height: 4rem;
}
@media (prefers-reduced-motion: no-preference) {
  .vw-logos img {
    transition: filter 200ms ease-out;
  }
}
.vw-logos img:hover {
  filter: grayscale(0);
}

/* Version 2 layout primitives. Children remain ordinary document nodes; the
   classes provide a safe responsive baseline without author supplied CSS. */
.vw-layout {
  display: flex;
  gap: var(--vw-layout-gap, 0);
  min-width: 0;
}
.vw-layout-stack,
.vw-layout-overlay {
  flex-direction: column;
}
.vw-layout-row {
  flex-wrap: wrap;
}
.vw-layout-grid {
  display: grid;
  grid-template-columns: repeat(var(--vw-layout-columns, 1), minmax(0, 1fr));
}
.vw-layout[data-vw-align="center"] {
  align-items: center;
}
.vw-layout[data-vw-align="end"] {
  align-items: flex-end;
}
.vw-layout[data-vw-align="stretch"] {
  align-items: stretch;
}
.vw-layout[data-vw-justify="center"] {
  justify-content: center;
}
.vw-layout[data-vw-justify="end"] {
  justify-content: flex-end;
}
.vw-layout[data-vw-justify="space-between"] {
  justify-content: space-between;
}
@media (max-width: 767px) {
  .vw-layout-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Geometry block (brand kit plan, 2026-09-06, Task 3): seed-of-life circles.
   Layout is unconditional; the draw/scroll entrance rules live in the MOTION
   block above (inside the reduced-motion gate, next to the five patterns)
   so a reduced-motion viewer sees the finished static art with no
   transition, and motionCss.test.js's single-block constraint holds. */
.vw-geometry {
  margin: 0 auto;
  line-height: 0;
}
.vw-geometry-sm {
  max-width: 12rem;
}
.vw-geometry-md {
  max-width: 20rem;
}
.vw-geometry-lg {
  max-width: 32rem;
}
.vw-geometry svg {
  width: 100%;
  height: auto;
  overflow: visible;
}

/* SESSION LIST — the live "what's on" rows (blocks/data.js). It shipped with
   no rules at all: every row rendered its cover photo at full width, so three
   sessions filled a screen. A row is a line, not a poster. */
.vw-session-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--vw-space-sm);
  max-width: var(--vw-container-base);
  margin-inline: auto;
}
.vw-session-item {
  display: grid;
  grid-template-columns: 4.5rem 1fr auto;
  align-items: center;
  gap: var(--vw-space-md);
  padding: var(--vw-space-sm);
  background: var(--vw-color-surface-raised);
  border: 1px solid var(--vw-color-border);
  border-radius: var(--vw-radius-md);
  text-align: start;
}
.vw-session-img {
  grid-row: span 2;
  width: 4.5rem;
  height: 4.5rem;
  object-fit: cover;
  border-radius: var(--vw-radius-sm);
}
/* A row without a cover keeps its columns: the text starts where the others'
   text starts, so a list of mixed rows still reads as a column. */
.vw-session-item:not(:has(.vw-session-img)) {
  grid-template-columns: 4.5rem 1fr auto;
}
.vw-session-when {
  grid-column: 2;
  font-size: var(--vw-size-xs);
  color: var(--vw-color-muted);
}
.vw-session-name {
  grid-column: 2;
  grid-row: 2;
  font-weight: 600;
}
.vw-session-where {
  grid-column: 2;
  grid-row: 3;
  font-size: var(--vw-size-xs);
  color: var(--vw-color-muted);
}
.vw-session-item .vw-btn {
  grid-column: 3;
  grid-row: 1 / span 3;
}
@media (max-width: 30rem) {
  .vw-session-item {
    grid-template-columns: 3.25rem 1fr;
  }
  .vw-session-img {
    width: 3.25rem;
    height: 3.25rem;
  }
  .vw-session-item .vw-btn {
    grid-column: 2;
    grid-row: 4;
    justify-self: start;
  }
}

/* A section's own alignment, for the blocks that carry none of their own —
   buttons above all. Two buttons under a centred intro used to sit flush
   against the inline start, and against each other. */
.vw-align-center .vw-section-inner {
  text-align: center;
}
.vw-align-start .vw-section-inner {
  text-align: start;
}
.vw-section-inner > .vw-btn + .vw-btn {
  margin-inline-start: var(--vw-space-sm);
}
