/* ============================================================================
   SMARTBOOKING — DESIGN SYSTEM
   ============================================================================

   A single stylesheet, organised as: tokens → base → primitives → components
   → sections → motion → responsive → overrides. Every page draws from the same
   component vocabulary, so a new page inherits the system by using the classes
   rather than by inventing styles.

   Three constraints shaped the system, and each one is load-bearing:

   1. ZERO EXTERNAL REQUESTS. No CDN, no web fonts, no fetched images. This keeps
      the app fast on parish-hall wi-fi, keeps a strict CSP achievable, and means
      the only third-party script in the entire product is Stripe's card iframe.
      Type is therefore a curated system stack; illustration is inline SVG.

   2. WCAG AA THROUGHOUT. Body text sits at 16px+ with a ~1.65 line height, and
      every foreground/background pair below was contrast-checked. Status is never
      communicated by colour alone — badges always carry a word.

   3. MOTION IS INFORMATION. Animation explains change: this is interactive, this
      just loaded, this arrived. Nothing bounces, spins, or auto-plays without a
      pause affordance, and the whole layer collapses under prefers-reduced-motion.

   ========================================================================= */

/* ============================================================================
   1. TOKENS
   ========================================================================= */

:root {
  color-scheme: light;

  /* ── Spacing — strict 8px system, with a 4px half-step for optical fixes ── */
  --s-1: 0.25rem;   /*  4px */
  --s-2: 0.5rem;    /*  8px */
  --s-3: 0.75rem;   /* 12px */
  --s-4: 1rem;      /* 16px */
  --s-5: 1.5rem;    /* 24px */
  --s-6: 2rem;      /* 32px */
  --s-7: 2.5rem;    /* 40px */
  --s-8: 3rem;      /* 48px */
  --s-9: 4rem;      /* 64px */
  --s-10: 5rem;     /* 80px */
  --s-11: 6rem;     /* 96px */
  --s-12: 8rem;     /* 128px */

  /* ── Radii ───────────────────────────────────────────────────────────── */
  --r-xs: 6px;
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 20px;
  --r-2xl: 28px;
  --r-full: 999px;

  /* ── Neutrals — violet-tinted rather than pure grey, so every page sits in
        the same colour family as the SmartBooking home page. Lightness kept
        from the previous (contrast-checked) values; only the hue moved. ──── */
  --ink-900: #14102a;
  --ink-800: #231d3e;
  --ink-700: #383255;
  --ink-600: #524b70;
  --ink-500: #686183;
  --ink-400: #8a82a2;
  --ink-300: #a69fbd;

  --surface-0: #ffffff;
  --surface-1: #faf9fd;
  --surface-2: #f4f1fa;
  --surface-3: #ebe6f5;

  --border: #e7e2f1;
  --border-strong: #d5cde5;

  /* ── Brand — SmartBooking purple, straight from the logo mark. The blue
        half of the mark arrives through --grad-brand on primary actions. ── */
  --brand-050: #f8f5fe;
  --brand-100: #efe9fc;
  --brand-200: #ded0f8;
  --brand-400: #8b5cf6;
  --brand-500: #7c3aed;
  --brand-600: #6c2bd9;
  --brand-700: #5522ac;
  --brand-800: #3f1a80;

  /* The logo's purple→blue sweep, defined once. Identical in both themes —
     a saturated gradient holds up on light and dark grounds alike. */
  --grad-brand: linear-gradient(135deg, #6c2bd9, #2e7cf6);

  /* ── Accent — brass, from the standing lamp. Used for emphasis, never
        for large fills; gold at scale looks cheap. ──────────────────────── */
  --gold-100: #f9f0df;
  --gold-200: #f0dfba;
  --gold-500: #c9973f;
  --gold-600: #a87a2c;
  --gold-700: #85601f;

  /* ── Semantic ────────────────────────────────────────────────────────── */
  --ok-100: #e4f0e9;
  --ok-600: #2c6b4c;
  --ok-700: #1f5138;

  --warn-100: #fbf0da;
  --warn-600: #8a5c11;
  --warn-700: #6b470c;

  --danger-100: #fbe9e7;
  --danger-600: #9c2f26;
  --danger-700: #7a231c;

  --info-100: #e6eff7;
  --info-600: #2a5578;
  --info-700: #1f4058;

  /* ── Elevation — layered, soft, warm-tinted. A single hard shadow always
        reads cheap; two stacked with different spreads reads considered. ── */
  --shadow-xs: 0 1px 2px rgba(20, 16, 42, 0.04);
  --shadow-sm: 0 1px 2px rgba(20, 16, 42, 0.04), 0 2px 6px -2px rgba(20, 16, 42, 0.06);
  --shadow-md: 0 2px 4px rgba(20, 16, 42, 0.04), 0 8px 20px -6px rgba(20, 16, 42, 0.10);
  --shadow-lg: 0 4px 8px rgba(20, 16, 42, 0.04), 0 18px 40px -12px rgba(20, 16, 42, 0.16);
  --shadow-xl: 0 8px 16px rgba(20, 16, 42, 0.05), 0 32px 64px -20px rgba(20, 16, 42, 0.22);
  --shadow-brand: 0 8px 24px -10px rgba(108, 43, 217, 0.45);

  --ring: 0 0 0 3px rgba(108, 43, 217, 0.25);

  /* ── Typography ──────────────────────────────────────────────────────────
        A serif display face against a sans interface face. The pairing is the
        point: a geometric-sans-everywhere page is the default look of every
        SaaS product, and a serif headline is what makes this read as an
        institution rather than a startup. Both are system stacks — see the
        zero-external-request constraint at the top of this file. ─────────── */
  --font-display: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
                  "Hoefler Text", Georgia, "Times New Roman", serif;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI",
               system-ui, Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;

  /* Fluid type scale. clamp() means no breakpoint jumps in headline sizes. */
  --t-xs: 0.75rem;
  --t-sm: 0.875rem;
  --t-base: 1rem;
  --t-md: 1.0625rem;
  --t-lg: 1.1875rem;
  --t-xl: clamp(1.25rem, 1.15rem + 0.4vw, 1.4rem);
  --t-2xl: clamp(1.5rem, 1.3rem + 0.9vw, 1.9rem);
  --t-3xl: clamp(1.85rem, 1.5rem + 1.6vw, 2.6rem);
  --t-4xl: clamp(2.2rem, 1.6rem + 2.8vw, 3.6rem);
  --t-5xl: clamp(2.6rem, 1.7rem + 4vw, 4.4rem);

  /* ── Motion ──────────────────────────────────────────────────────────── */
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --dur-fast: 140ms;
  --dur: 240ms;
  --dur-slow: 460ms;

  /* ── Layout ──────────────────────────────────────────────────────────── */
  --wrap: 1200px;
  --wrap-tight: 880px;
  --wrap-prose: 68ch;
  --nav-h: 72px;

  /* ── Aliases kept so existing page markup keeps working ──────────────── */
  --page: var(--surface-1);
  --surface: var(--surface-0);
  --surface-sunk: var(--surface-2);
  --ink: var(--ink-900);
  --ink-2: var(--ink-600);
  --ink-3: var(--ink-500);
  --muted: var(--ink-400);
  --accent: var(--brand-600);
  --accent-deep: var(--brand-700);
  --accent-soft: var(--brand-100);
  --accent-line: var(--brand-200);
  --gold: var(--gold-600);
  --gold-soft: var(--gold-100);
  --rule: var(--border);
  --rule-strong: var(--border-strong);
  --shadow: var(--shadow-md);
  --radius: var(--r-md);
  --radius-lg: var(--r-lg);
  --ok: var(--ok-600);
  --ok-soft: var(--ok-100);
  --warn: var(--warn-600);
  --warn-soft: var(--warn-100);
  --danger: var(--danger-600);
  --danger-soft: var(--danger-100);
  --info: var(--info-600);
  --info-soft: var(--info-100);
  --wash: rgba(20, 16, 42, 0.04);

  /* Frosted panel recipe, so glass is defined once rather than per component. */
  --glass-bg: rgba(255, 255, 255, 0.72);
  --glass-border: rgba(255, 255, 255, 0.65);
  --glass-blur: saturate(1.6) blur(14px);
}

/* ── Dark theme ────────────────────────────────────────────────────────────
   Not an inversion. Surfaces stay warm, the brand lightens enough to hold
   contrast on dark, and shadows give way to borders — a dark surface cannot
   show a shadow, so elevation has to be expressed as a lighter edge. ────── */
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) { color-scheme: dark; }
}

:root[data-theme="dark"],
:root:where(:not([data-theme="light"])) {
  --dark-defs: initial;
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    --ink-900: #f5f3fb;
    --ink-800: #e9e6f4;
    --ink-700: #d7d2e7;
    --ink-600: #bcb5d4;
    --ink-500: #a29bc2;
    --ink-400: #857ea6;
    --ink-300: #67608b;

    --surface-0: #171331;
    --surface-1: #0f0c22;
    --surface-2: #1e1940;
    --surface-3: #29234f;

    --border: #2e2852;
    --border-strong: #3f3870;

    --brand-050: #1d1537;
    --brand-100: #261c49;
    --brand-200: #392a68;
    --brand-400: #a586f5;
    --brand-500: #b59bf7;
    --brand-600: #c3adf9;
    --brand-700: #d4c4fb;
    --brand-800: #e6dcfd;

    --gold-100: #2a2116;
    --gold-200: #3d2f1d;
    --gold-500: #d9b062;
    --gold-600: #e4c17c;
    --gold-700: #eed093;

    --ok-100: #1a2b22;   --ok-600: #7cc39c;   --ok-700: #99d4b2;
    --warn-100: #2c2415; --warn-600: #dcb26a; --warn-700: #e8c78d;
    --danger-100: #2e1a18; --danger-600: #e79b93; --danger-700: #f0b6b0;
    --info-100: #17232d;  --info-600: #8fb6d6;  --info-700: #accbe4;

    --shadow-xs: none;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
    --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.5);
    --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
    --shadow-xl: 0 24px 56px rgba(0, 0, 0, 0.65);
    --shadow-brand: 0 8px 24px -10px rgba(195, 173, 249, 0.3);
    --ring: 0 0 0 3px rgba(195, 173, 249, 0.35);

    --glass-bg: rgba(23, 19, 49, 0.72);
    --glass-border: rgba(255, 255, 255, 0.08);
    --wash: rgba(255, 255, 255, 0.05);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --ink-900: #f5f3fb;
  --ink-800: #e9e6f4;
  --ink-700: #d7d2e7;
  --ink-600: #bcb5d4;
  --ink-500: #a29bc2;
  --ink-400: #857ea6;
  --ink-300: #67608b;

  --surface-0: #171331;
  --surface-1: #0f0c22;
  --surface-2: #1e1940;
  --surface-3: #29234f;

  --border: #2e2852;
  --border-strong: #3f3870;

  --brand-050: #1d1537;
  --brand-100: #261c49;
  --brand-200: #392a68;
  --brand-400: #a586f5;
  --brand-500: #b59bf7;
  --brand-600: #c3adf9;
  --brand-700: #d4c4fb;
  --brand-800: #e6dcfd;

  --gold-100: #2a2116;
  --gold-200: #3d2f1d;
  --gold-500: #d9b062;
  --gold-600: #e4c17c;
  --gold-700: #eed093;

  --ok-100: #1a2b22;   --ok-600: #7cc39c;   --ok-700: #99d4b2;
  --warn-100: #2c2415; --warn-600: #dcb26a; --warn-700: #e8c78d;
  --danger-100: #2e1a18; --danger-600: #e79b93; --danger-700: #f0b6b0;
  --info-100: #17232d;  --info-600: #8fb6d6;  --info-700: #accbe4;

  --shadow-xs: none;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.5);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
  --shadow-xl: 0 24px 56px rgba(0, 0, 0, 0.65);
  --shadow-brand: 0 8px 24px -10px rgba(195, 173, 249, 0.3);
  --ring: 0 0 0 3px rgba(195, 173, 249, 0.35);

  --glass-bg: rgba(23, 19, 49, 0.72);
  --glass-border: rgba(255, 255, 255, 0.08);
  --wash: rgba(255, 255, 255, 0.05);
}

/* ============================================================================
   2. BASE
   ========================================================================= */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + var(--s-4));
}

body {
  margin: 0;
  background: var(--surface-1);
  color: var(--ink-900);
  font-family: var(--font-sans);
  font-size: var(--t-md);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.018em;
  margin: 0 0 var(--s-3);
  color: var(--ink-900);
  text-wrap: balance;
}

h1 { font-size: var(--t-4xl); }
h2 { font-size: var(--t-3xl); }
h3 { font-size: var(--t-xl); }
h4 { font-size: var(--t-lg); font-family: var(--font-sans); font-weight: 650; letter-spacing: -0.005em; }
h5 { font-size: var(--t-base); font-family: var(--font-sans); font-weight: 650; }

p { margin: 0 0 var(--s-4); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

a {
  color: var(--brand-600);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--dur-fast) var(--ease-out);
}

a:hover { color: var(--brand-700); }

/* Focus is never removed. It is the only way a keyboard user knows where they
   are, and this platform is used by people who do not use a mouse. */
:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

img, svg, video { max-width: 100%; display: block; }

hr { border: 0; border-top: 1px solid var(--border); margin: var(--s-6) 0; }

::selection { background: var(--brand-200); color: var(--ink-900); }

/* ============================================================================
   3. LAYOUT PRIMITIVES
   ========================================================================= */

.wrap { width: min(100% - var(--s-6), var(--wrap)); margin-inline: auto; }
.wrap-narrow { width: min(100% - var(--s-6), var(--wrap-tight)); margin-inline: auto; }
.prose { max-width: var(--wrap-prose); }

.page-body { min-height: 60vh; padding: var(--s-8) 0 var(--s-11); }

/* Section rhythm. Landing sections breathe; app pages stay tighter. */
.section { padding: var(--s-11) 0; position: relative; }
.section-sm { padding: var(--s-9) 0; }

/* A results band opens directly under the panel that filters it, so the two are
   one interaction and a 64px trench between them reads as a page break. The
   bottom keeps its full measure — that gap is the end of the page, not a join.

   Measured, not guessed: with --s-9 top the first event card began at y=628 on a
   768px viewport, entirely below the fold on a laptop. */
.section-sm.section-results { padding-top: var(--s-6); }
.section-tint { background: var(--surface-2); }
.section-line { border-top: 1px solid var(--border); }

.stack > * + * { margin-top: var(--s-4); }
.stack-sm > * + * { margin-top: var(--s-2); }
.stack-lg > * + * { margin-top: var(--s-6); }
.stack-xl > * + * { margin-top: var(--s-8); }

.row { display: flex; gap: var(--s-4); align-items: center; flex-wrap: wrap; }
.row-between { display: flex; gap: var(--s-4); align-items: center; justify-content: space-between; flex-wrap: wrap; }
.row-tight { gap: var(--s-2); }
.grow { flex: 1 1 auto; }

.grid { display: grid; gap: var(--s-5); }
.grid-cards { grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }
.grid-stats { grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); }

.filter-grid {
  grid-template-columns: minmax(220px, 2fr) minmax(160px, 1fr) minmax(180px, 1fr) auto;
  align-items: end;
  gap: var(--s-4);
}

/* .field carries a bottom margin for stacked forms, which is right everywhere except
   here: the row aligns on `end`, so that margin lifts every input 24px above the submit
   button and the row reads as broken. The gap already provides the spacing a grid needs. */
.filter-grid .field { margin-bottom: 0; }

/* A <fieldset> is the right element for a group of related controls — it names the group
   in the accessibility tree — but its default border, padding and inline-size behaviour
   are from 1996. Reset to a plain block and let the surrounding .card provide the frame. */
.filter-set { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
.filter-set > legend { padding: 0; margin-bottom: var(--s-4); }

/* A page whose first element is a full-bleed band drops the top padding, so the band meets
   the masthead instead of floating below a strip of page surface. */
.page-body-flush { padding-top: 0; }

/* Available to a screen reader, absent from the page.
   For a <caption> that names what a table contains: sighted readers already have the heading
   above it, and a reader hearing "table" with no name has to infer it from the first cell.
   Not display:none and not visibility:hidden — both remove it from the accessibility tree,
   which is the one place it needs to exist. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The hero's default rhythm is tuned for the marketing page, where the band IS the screen.
   On a booking page the same padding pushes the ticket panel and its price to the fold on a
   laptop, which is the one thing a buyer came to see. Tighter top and bottom, same design.

   Two class names on purpose: .hero is declared later in the file, in section 12, so a
   single-class selector here would lose on source order despite being the more specific
   intent. */
.hero.hero-compact { padding: var(--s-8) 0 var(--s-7); }

/* A heading that leads a content section inside a card. .card-title is --t-lg, which is
   right for a compact card header but leaves only 2px between a section heading and the
   body text under it — a difference nobody reads as hierarchy. --t-2xl is the step the
   scale already provides between the two.

   Both class names again: .card-title is declared later, in section 5, so a single-class
   selector loses on source order — the same trap as .hero.hero-compact above. */
.card-title.card-title-lg { font-size: var(--t-2xl); }

/* The panel that follows the reader holds the price and the primary action, and it was
   rendering with exactly the same background, border and shadow as the prose card beside
   it — measured identical. A booking page's ticket panel should sit above its content, not
   level with it. Existing tokens only: a firmer edge and the next shadow step, no new
   colour, and both themes get it because both define the tokens.

   Two class names because .card sets border-color and box-shadow later in this file. */
.card.sticky-panel { border-color: var(--border-strong); box-shadow: var(--shadow-md); }

/* ── Event detail page ───────────────────────────────────────────────────────
   Everything below is scoped to a class the event page adds. Nothing here
   redefines a shared component, because .figure, .figure-value and .hero-title
   are all on the landing page too and that page must not change. */

/* A figure holding PROSE rather than a metric.

   .figure-value is 2.2rem of display type, and it is right for what it was built
   for: "1,240", "99.9%" — a number that IS the content. The event page reuses
   .figure for an organiser's name, a long date and a venue. At 2.2rem
   "Riverside Community Group" wraps to three lines of display type and outweighs
   the h1 beside it, and the six values under "Event details" become a wall with
   no hierarchy left between them.

   Body scale and the sans face instead, so the eye reads label → value and moves
   on. A separate modifier rather than a change to .figure-value: the landing
   page's statistics use that rule and are correct as they are. */
.figure-sm .figure-value {
  font-family: var(--font-sans);
  font-size: var(--t-md);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: normal;
}

/* An event title is content of unknown length, not a marketing headline. --t-5xl
   reaches 4.4rem, which a three-word title carries and a real one — "St Thomas
   Parish Annual Community Lunch" — does not: it becomes four lines that push the
   ticket panel's price below the fold on a laptop.

   --t-3xl tops out at 2.6rem and keeps a clear step over the --t-2xl section
   headings under it. Scoped to .hero-compact, which is the booking pages' hero. */
.hero-compact .hero-title { font-size: var(--t-3xl); margin-bottom: var(--s-4); }
.hero-compact .hero-lede { font-size: var(--t-md); margin-bottom: var(--s-5); }

/* The hero figures sat 40px apart in a row that had room for far less once the
   values came down to body scale; 40px of air between three short labels reads
   as three unrelated things rather than one summary line. */
.hero-compact .hero-figures { gap: var(--s-4) var(--s-7); padding-top: var(--s-4); }

/* Two columns of facts, always. .grid-2's auto-fit needs 280px per column and the
   right-hand content column of .grid-split is narrower than that below about
   1200px, so six facts were stacking into six full-width rows on most laptops.
   Fixed at two, collapsing to one where two genuinely will not fit. */
.detail-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-5) var(--s-4); }

@media (max-width: 560px) {
  .detail-facts { grid-template-columns: 1fr; }
}

/* ── Ticket panel ──────────────────────────────────────────────────────────
   .tier carries its own 16px padding inside a .card-pad that already has 24px,
   so every tier's text started 16px to the right of the panel's heading, price
   and button. Pulling the row out by its own inline padding puts the text back
   on the card's edge while the hover surface still extends past it. */
.tier-list .tier {
  padding-inline: var(--s-3);
  margin-inline: calc(var(--s-3) * -1);
  gap: var(--s-3) var(--s-4);
  align-items: flex-start;
}

/* Price above quantity, both hard right. As flat siblings of the name these were
   wrapping items in a flex row: the price floated to wherever there was room and
   the "Up to N" hint fell onto its own line. One column keeps them together and
   gives the panel a single alignment edge down the right. */
.tier-buy {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s-2);
  flex: 0 0 auto;
  text-align: right;

  /* .tier still wraps, and it should: in the 464px sticky panel a tier name and a
     tier description have no room to share a line with a price and a stepper. But
     a wrapped flex line starts at the LEFT, which put the price 202px inside the
     panel's right edge — measured — while the same price sat flush right whenever
     the panel was full width. Two different alignments for one component.

     An auto left margin eats the free space on whichever line this lands on, so
     the price and the stepper meet the panel's right edge in both layouts. */
  margin-left: auto;
}

/* .hint carries a top margin for use under a field. Inside a gapped column that
   margin is applied twice over. */
.tier-buy .hint { margin-top: 0; }

/* Tiers are the panel's content, not a list appended to it: the same --s-4 that
   separates the heading from the body separates them from the total. */
.tier-list { margin-block: var(--s-4) var(--s-5); }

/* ── Ticket categories ───────────────────────────────────────────────────
   With one tier per event a plain rule between rows was enough. With Adult,
   Child and Under 10 stacked, the buyer is comparing three things and needs to
   see where one ends and the next begins — so each becomes a card rather than a
   band, and the hover state that used to wash the whole row now belongs to a
   shape with edges. */
.tier-list .tier {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-0);
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}

.tier-list .tier + .tier { margin-top: var(--s-3); }

/* .tier:last-child zeroes the bottom border for the band layout, and it is declared later in this
   file — so without restoring it here the final card renders with three sides. Three class-level
   selectors to beat it rather than !important. */
.tier-list .tier:last-child { border-bottom: 1px solid var(--border); }

.tier-list .tier:hover { background: var(--surface-2); border-color: var(--border-strong); }

/* The eligibility line — "18+ years", "10–17 years". It is the thing that tells
   a buyer which category they are in, and as plain .tier-desc it read as an
   afterthought under the name. */
.tier-desc { margin-top: 2px; }

/* A free category says so in a word. "AUD $0.00" is arithmetically the same and
   reads as a price somebody still has to think about. */
.price-free { font-weight: 700; font-size: var(--t-lg); letter-spacing: 0.02em; }

/* ── Order summary ───────────────────────────────────────────────────────
   The arithmetic behind the total, shown only once the buyer has chosen
   something. Reuses .summary-row from the checkout and payment steps so a line
   reads identically at every step of the journey. */
.order-summary {
  padding: var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.order-summary dl { margin: 0; }
.order-summary .stat-label { margin-bottom: var(--s-3); display: block; }

/* Appears the moment a quantity leaves zero, so it earns a transition rather
   than snapping in. Opacity only — a height animation on a list that grows by a
   row at a time judders. */
.js-motion .order-summary { animation: rise var(--dur-fast) var(--ease-out) both; }

/* The total and the action it commits you to. .stack-sm's 8px put the button
   against the amount with no pause between reading a figure and pressing a
   button — the one place on this page where a pause is worth having. */
.buy-actions .btn { margin-top: var(--s-4); }
.buy-actions .hint { text-align: center; }

@media (max-width: 900px) {
  .filter-grid { grid-template-columns: 1fr; }
  .filter-grid .row { justify-content: stretch; }
  .filter-grid .btn { flex: 1 1 auto; }
}

.grid-split {
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: var(--s-7);
  align-items: start;
}

@media (max-width: 960px) {
  .grid-split { grid-template-columns: 1fr; }
  .grid-split .sticky-panel { position: static !important; }
}

.sticky-panel { position: sticky; top: calc(var(--nav-h) + var(--s-4)); }

/* ── Section headings ────────────────────────────────────────────────────── */

.section-head { max-width: 46rem; margin-bottom: var(--s-7); }
.section-head.is-centred { margin-inline: auto; text-align: center; }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-sans);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--brand-600);
  margin: 0 0 var(--s-3);
}

.eyebrow::before {
  content: "";
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--brand-600), var(--gold-500));
}

.section-head.is-centred .eyebrow { justify-content: center; }

.lede {
  font-size: var(--t-lg);
  color: var(--ink-600);
  line-height: 1.6;
  margin: 0;
  max-width: 46rem;
}

.section-head.is-centred .lede { margin-inline: auto; }

.page-head { margin-bottom: var(--s-7); }
.page-head h1 { margin-bottom: var(--s-2); }

/* ── Listing header ──────────────────────────────────────────────────────
   The line above a grid of results. It was a bare .eyebrow — 14px, uppercase,
   brand-coloured — doing the job of a section heading, which left the listing
   band opening with nothing the eye could land on.

   The count is the heading, at reading size and weight, with the qualifier
   ("coming up", "found") demoted beside it. A rule fills the remaining width so
   the band reads as a titled section rather than a caption floating over cards. */
.listing-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
}

.listing-head h2 {
  font-family: var(--font-sans);
  font-size: var(--t-lg);
  font-weight: 650;
  letter-spacing: -0.005em;
  margin: 0;
  flex: 0 0 auto;
}

.listing-head .listing-qualifier { font-size: var(--t-sm); color: var(--ink-500); flex: 0 0 auto; }

/* Decorative and inert to assistive technology: the heading beside it already
   says what the section is. */
.listing-head::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--border);
}

.crumbs { font-size: var(--t-sm); color: var(--ink-500); margin-bottom: var(--s-3); }
.crumbs a { color: var(--ink-500); text-decoration: none; }
.crumbs a:hover { color: var(--brand-600); text-decoration: underline; }

/* ============================================================================
   3b. COMPONENTS — ICONS
   ========================================================================= */

/* _Icon.cshtml emits width="24" height="24" on every glyph and there was no
   rule here at all, so an icon rendered 24px in every context and — the part
   that actually cost layout — sat on the TEXT BASELINE as an inline element.

   A 24px baseline-aligned SVG inside a 14px/23px line box makes the line 47px
   tall, because the glyph's bottom lands on the baseline and the descender
   space is added underneath it. Two such lines in a listing card measured 98px
   for two short strings. That is where "the cards feel empty" came from: not
   spacing tokens, an unstyled icon.

   `flex: 0 0 auto` stops an icon being squashed inside a flex row; the button
   rule below already said so for buttons, and it belongs to icons generally. */
.icon {
  flex: 0 0 auto;
  vertical-align: middle;
}

/* Icons paired with small text — meta lines, hints, inline labels. 24px next to
   14px text reads as an icon with a caption rather than a labelled icon. */
.icon-sm { width: 16px; height: 16px; }

/* ============================================================================
   4. COMPONENTS — BUTTONS
   ========================================================================= */

.btn {
  --btn-bg: transparent;
  --btn-fg: var(--ink-900);
  --btn-border: transparent;

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  /* 44px minimum target: used one-handed at a parish hall door. */
  min-height: 44px;
  padding: var(--s-3) var(--s-5);
  border-radius: var(--r-md);
  border: 1px solid var(--btn-border);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font: inherit;
  font-size: var(--t-base);
  font-weight: 600;
  letter-spacing: -0.005em;
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur) var(--ease-out);
}

.btn:active { transform: translateY(1px); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; transform: none; }

.btn svg { flex: 0 0 auto; }

.btn-primary {
  /* The home page's purple→blue sweep, so the primary action reads as the same
     product on every page. --btn-bg stays underneath as the painted fallback,
     and the gradient is identical in dark mode, so the text is white in both. */
  --btn-bg: var(--brand-600);
  --btn-fg: #fff;
  background-image: var(--grad-brand);
  box-shadow: var(--shadow-sm);
}

:root[data-theme="dark"] .btn-primary { --btn-fg: #fff; }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .btn-primary { --btn-fg: #fff; }
}

.btn-primary:hover:not(:disabled) {
  filter: brightness(1.08);
  transform: translateY(-2px);
  box-shadow: var(--shadow-brand);
}

.btn-primary:active:not(:disabled) { transform: translateY(0); }

/* A single sheen sweep on the primary action only. Restraint is the point —
   if every button glints, none of them reads as primary. */
.btn-primary::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 -130%;
  width: 55%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.28), transparent);
  transform: skewX(-18deg);
  pointer-events: none;
}

.btn-primary:hover:not(:disabled)::after { transition: left var(--dur-slow) var(--ease-out); left: 150%; }

.btn-secondary {
  --btn-bg: var(--surface-0);
  --btn-fg: var(--ink-800);
  --btn-border: var(--border-strong);
  box-shadow: var(--shadow-xs);
}

.btn-secondary:hover:not(:disabled) {
  --btn-bg: var(--surface-2);
  --btn-border: var(--ink-300);
  transform: translateY(-2px);
  box-shadow: var(--shadow-sm);
}

.btn-ghost { --btn-fg: var(--brand-600); }
.btn-ghost:hover:not(:disabled) { --btn-bg: var(--brand-100); }

.btn-danger { --btn-bg: var(--danger-600); --btn-fg: #fff; }
.btn-danger:hover:not(:disabled) { --btn-bg: var(--danger-700); transform: translateY(-2px); }

.btn-lg { min-height: 54px; padding: var(--s-4) var(--s-6); font-size: var(--t-md); border-radius: var(--r-lg); }
.btn-sm { min-height: 36px; padding: var(--s-2) var(--s-3); font-size: var(--t-sm); border-radius: var(--r-sm); }
.btn-block { width: 100%; }

/* Google's mark on the sign-in button.
   KEPT AT ITS OWN COLOURS AND NEVER RECOLOURED: Google's brand guidelines require it, so this
   sets a size and nothing else — no fill, no filter, no opacity change on hover. flex-shrink
   stops it squashing when the label wraps on a narrow phone. */
.google-mark {
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
}

/* Arrow nudges forward on hover — a directional cue on "continue" actions. */
.btn .btn-arrow { transition: transform var(--dur) var(--ease-out); }
.btn:hover .btn-arrow { transform: translateX(3px); }

/* ============================================================================
   5. COMPONENTS — CARDS
   ========================================================================= */

.card {
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}

.card-flush { padding: 0; overflow: hidden; }
.card-pad { padding: var(--s-5); }
.card-lg { padding: var(--s-6); border-radius: var(--r-xl); }

/* Opt-in lift. Not every card is interactive, and a static card that rises on
   hover promises a click that never happens. */
.card-hover:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
  border-color: var(--border-strong);
}

.card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
  margin-bottom: var(--s-4);
  flex-wrap: wrap;
}

.card-title {
  margin: 0;
  font-size: var(--t-lg);
  font-family: var(--font-sans);
  font-weight: 650;
  letter-spacing: -0.008em;
}

/* Frosted panel. Used only over busy backgrounds where the blur genuinely
   separates layers — glass over a flat surface is decoration, not design. */
.glass {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
}

/* ============================================================================
   6. COMPONENTS — BADGES
   ========================================================================= */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-full);
  font-size: var(--t-xs);
  font-weight: 650;
  letter-spacing: 0.005em;
  white-space: nowrap;
  border: 1px solid transparent;
  line-height: 1.5;
}

.badge-ok      { background: var(--ok-100); color: var(--ok-700); border-color: color-mix(in srgb, var(--ok-600) 26%, transparent); }
.badge-warn    { background: var(--warn-100); color: var(--warn-700); border-color: color-mix(in srgb, var(--warn-600) 26%, transparent); }
.badge-danger  { background: var(--danger-100); color: var(--danger-700); border-color: color-mix(in srgb, var(--danger-600) 26%, transparent); }
.badge-info    { background: var(--info-100); color: var(--info-700); border-color: color-mix(in srgb, var(--info-600) 26%, transparent); }
.badge-neutral { background: var(--surface-2); color: var(--ink-600); border-color: var(--border); }
.badge-accent  { background: var(--brand-100); color: var(--brand-700); border-color: var(--brand-200); }

/* Dot prefix gives a second, non-colour signal at a glance. */
.badge-dot::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* ============================================================================
   7. COMPONENTS — FORMS
   ========================================================================= */

.field { display: block; margin-bottom: var(--s-5); }

.field label, .label {
  display: block;
  font-size: var(--t-sm);
  font-weight: 650;
  color: var(--ink-700);
  margin-bottom: var(--s-2);
  letter-spacing: -0.003em;
}

.hint { font-size: var(--t-sm); color: var(--ink-500); margin-top: var(--s-2); }

.input, .select, .textarea {
  width: 100%;
  min-height: 46px;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-0);
  color: var(--ink-900);
  font: inherit;
  font-size: var(--t-base);
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}

.textarea { min-height: 120px; resize: vertical; line-height: 1.6; }

.input::placeholder, .textarea::placeholder { color: var(--ink-400); }

.input:hover:not(:focus), .select:hover:not(:focus), .textarea:hover:not(:focus) {
  border-color: var(--ink-300);
}

.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--brand-600);
  box-shadow: var(--ring);
}

.input[aria-invalid="true"], .select[aria-invalid="true"] { border-color: var(--danger-600); }

.field-error { color: var(--danger-600); font-size: var(--t-sm); margin-top: var(--s-2); font-weight: 600; }

/* asp-validation-summary ALWAYS emits its div — when the model is valid it renders
   class="validation-summary-valid" wrapping an empty <ul>. With .alert-danger on it that was a
   bare red 50px box sitting above the card form on every clean page load, which on a payment
   screen reads as "something is already wrong".

   Hidden rather than restyled: an error summary with nothing in it has nothing to say. The
   framework swaps the class to validation-summary-errors as soon as there is something, and the
   box appears then. */
.validation-summary-valid { display: none !important; }

/* ── Required marker ─────────────────────────────────────────────────────
   Every field on the checkout was required and none of them said so, which
   leaves "required" to be discovered by submitting and being refused.

   A word, not a bare asterisk. An asterisk alone needs a legend elsewhere on
   the page to decode it, and screen readers either skip it or read "star".
   .req-mark hides the glyph from assistive technology and the .sr-only text
   beside it says the word — so it is announced as "Email required". */
.req-mark {
  color: var(--danger-600);
  font-weight: 700;
  margin-left: 2px;
}

/* The counterpart: fields that are genuinely optional say so, in the same
   place, so a blank one is not read as an oversight. */
.optional-mark {
  color: var(--ink-500);
  font-weight: 400;
  font-size: var(--t-xs);
  margin-left: var(--s-2);
}

.checkline { display: flex; gap: var(--s-3); align-items: flex-start; }
/* 24px is the WCAG 2.5.8 minimum. The <label> wraps the box and its text, so the
   real target is the whole row — but the box is what a thumb aims at. */
.checkline input[type="checkbox"] { width: 24px; height: 24px; margin-top: 1px; accent-color: var(--brand-600); flex: 0 0 auto; }

/* The quantity box. Tabular figures so the width does not jump between 1 and 2
   places. font-weight because the number a buyer is choosing should read as the
   value it is, not as placeholder text. */
.qty-input {
  width: 92px;
  min-width: 92px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 650;
}

/* Search field with a leading icon slot. */
.field-icon { position: relative; }
.field-icon .input { padding-left: calc(var(--s-4) + 26px); }
.field-icon > svg {
  position: absolute;
  left: var(--s-4);
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-400);
  pointer-events: none;
}

/* ============================================================================
   8. COMPONENTS — TABLES
   ========================================================================= */

/* Wide tables scroll inside their own box; the page body never scrolls sideways. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.table { width: 100%; border-collapse: collapse; font-size: var(--t-base); }

.table th {
  text-align: left;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-500);
  font-weight: 700;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
  background: var(--surface-2);
}

.table td { padding: var(--s-4); border-bottom: 1px solid var(--border); vertical-align: middle; }
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background-color var(--dur-fast) var(--ease-out); }
.table tbody tr:hover { background: var(--surface-2); }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }

.mono { font-family: var(--font-mono); font-size: 0.92em; letter-spacing: 0.02em; }

/* ============================================================================
   9. COMPONENTS — STATS
   ========================================================================= */

.stat {
  position: relative;
  overflow: hidden;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  transition: box-shadow var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}

.stat:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--border-strong); }

/* Accent hairline drawn across the top on hover. */
.stat::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, var(--brand-600), var(--gold-500));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-slow) var(--ease-out);
}

.stat:hover::before { transform: scaleX(1); }

.stat-label {
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-500);
  font-weight: 700;
  margin-bottom: var(--s-2);
}

.stat-value {
  font-family: var(--font-display);
  font-size: 2.1rem;
  font-weight: 600;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.stat-note { font-size: var(--t-sm); color: var(--ink-500); margin-top: var(--s-2); }

/* ============================================================================
   10. COMPONENTS — ALERTS & EMPTY STATES
   ========================================================================= */

.alert {
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-md);
  border: 1px solid;
  border-left-width: 3px;
  font-size: var(--t-base);
}

.alert-ok     { background: var(--ok-100); border-color: var(--ok-600); color: var(--ok-700); }
.alert-warn   { background: var(--warn-100); border-color: var(--warn-600); color: var(--warn-700); }
.alert-danger { background: var(--danger-100); border-color: var(--danger-600); color: var(--danger-700); }
.alert-info   { background: var(--info-100); border-color: var(--info-600); color: var(--info-700); }

.alert strong { display: block; margin-bottom: var(--s-1); }

.empty {
  text-align: center;
  padding: var(--s-9) var(--s-5);
  color: var(--ink-500);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-xl);
  background: var(--surface-0);
  transition: border-color var(--dur) var(--ease-out);
}

.empty:hover { border-color: var(--brand-200); }
.empty h3 { color: var(--ink-700); }

/* ============================================================================
   11. NAVIGATION
   ========================================================================= */

.masthead {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

/* The bar earns its border and shadow only once there is content behind it. */
.masthead.is-stuck { border-bottom-color: var(--border); box-shadow: var(--shadow-sm); }

.masthead-inner {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  min-height: var(--nav-h);
  padding-block: var(--s-2);
}

.brand { display: inline-flex; align-items: center; gap: var(--s-3); text-decoration: none; color: var(--ink-900); flex: 0 0 auto; }
.brand:hover { color: var(--ink-900); }

.brand-mark {
  width: 40px; height: 40px;
  flex: 0 0 auto;
  color: var(--brand-600);
  transition: transform var(--dur-slow) var(--ease-spring);
}

.brand:hover .brand-mark { transform: rotate(-6deg) scale(1.06); }

.brand-text { display: flex; flex-direction: column; line-height: 1.12; }

.brand-name { font-family: var(--font-display); font-size: 1.28rem; font-weight: 600; letter-spacing: -0.02em; }
.brand-sub { font-size: 0.66rem; color: var(--ink-500); letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600; }

.masthead-nav { display: flex; gap: var(--s-1); align-items: center; margin-left: auto; flex-wrap: wrap; }

.navlink {
  position: relative;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  color: var(--ink-600);
  text-decoration: none;
  font-size: var(--t-base);
  font-weight: 550;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}

.navlink:hover { color: var(--ink-900); background: var(--wash); }

.navlink::after {
  content: "";
  position: absolute;
  left: 50%; right: 50%; bottom: 2px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--brand-600), var(--gold-500));
  transition: left var(--dur) var(--ease-out), right var(--dur) var(--ease-out);
}

.navlink:hover::after,
.navlink[aria-current="page"]::after { left: var(--s-3); right: var(--s-3); }
.navlink[aria-current="page"] { color: var(--brand-600); }

.icon-btn {
  /* Never shrink. As a flex child it is the item most willing to give up space, and a
     40px control squeezed to 14px is both unusable and wide enough to push the row past
     the viewport. Crowding must be resolved by what the bar contains, not by deforming
     its controls. */
  flex: 0 0 auto;
  width: 40px; height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-600);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}

.icon-btn:hover { background: var(--wash); color: var(--ink-900); transform: translateY(-1px); }

/* ── Organisation portal context bar ─────────────────────────────────────── */

.portal-bar {
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  padding: var(--s-2) 0;
  font-size: var(--t-sm);
}

.portal-bar .wrap { display: flex; gap: var(--s-4); align-items: center; flex-wrap: wrap; }
.portal-org { font-weight: 650; display: inline-flex; align-items: center; gap: var(--s-2); }

.subnav { display: flex; gap: var(--s-1); flex-wrap: wrap; margin-left: auto; }

.subnav a {
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  color: var(--ink-600);
  text-decoration: none;
  font-weight: 600;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}

.subnav a:hover { background: var(--surface-0); color: var(--ink-900); }
.subnav a[aria-current="page"] { background: var(--brand-600); color: #fff; }
:root[data-theme="dark"] .subnav a[aria-current="page"] { color: var(--surface-0); }

/* ============================================================================
   12. HERO
   ========================================================================= */

.hero {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding: var(--s-11) 0 var(--s-10);
  border-bottom: 1px solid var(--border);
}

.hero-inner { position: relative; z-index: 1; }

/* Decorative mesh. Three very low-opacity blurred shapes drifting on long
   cycles — depth, not animation. Inert to pointer events, behind content. */
.mesh { position: absolute; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; }

.mesh span {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.5;
}

.mesh span:nth-child(1) {
  width: 520px; height: 520px;
  top: -220px; right: -120px;
  background: radial-gradient(circle, color-mix(in srgb, var(--brand-500) 40%, transparent), transparent 70%);
  animation: drift-a 24s ease-in-out infinite alternate;
}

.mesh span:nth-child(2) {
  width: 420px; height: 420px;
  bottom: -240px; left: -140px;
  background: radial-gradient(circle, color-mix(in srgb, var(--gold-500) 34%, transparent), transparent 70%);
  animation: drift-b 30s ease-in-out infinite alternate;
}

.mesh span:nth-child(3) {
  width: 340px; height: 340px;
  top: 30%; left: 45%;
  background: radial-gradient(circle, color-mix(in srgb, var(--brand-400) 22%, transparent), transparent 70%);
  animation: drift-c 27s ease-in-out infinite alternate;
}

@keyframes drift-a { to { transform: translate3d(-50px, 40px, 0) scale(1.14); } }
@keyframes drift-b { to { transform: translate3d(60px, -34px, 0) scale(1.1); } }
@keyframes drift-c { to { transform: translate3d(-40px, -50px, 0) scale(1.18); } }

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: var(--s-9);
  align-items: center;
}

@media (max-width: 1000px) {
  .hero-grid { grid-template-columns: 1fr; gap: var(--s-7); }
}

.hero-title { font-size: var(--t-5xl); margin-bottom: var(--s-5); letter-spacing: -0.03em; }
.hero-title span { display: block; }

/* Gradient on the emphasis line only. A fully gradient headline is the single
   most over-used effect in SaaS design and hurts contrast; one line earns it. */
.hero-title .accent-line {
  background: linear-gradient(100deg, var(--brand-600) 10%, var(--gold-600) 90%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Fallback for anything that cannot clip to text — never a transparent heading. */
  -webkit-text-fill-color: transparent;
}

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .hero-title .accent-line { color: var(--brand-600); -webkit-text-fill-color: currentColor; }
}

.hero-lede { font-size: var(--t-lg); color: var(--ink-600); max-width: 34rem; margin-bottom: var(--s-6); }

.hero-cta { display: flex; gap: var(--s-3); flex-wrap: wrap; margin-bottom: var(--s-7); }

.hero-figures { display: flex; flex-wrap: wrap; gap: var(--s-7); padding-top: var(--s-5); border-top: 1px solid var(--border); }

.figure { display: flex; flex-direction: column; }

.figure-value {
  font-family: var(--font-display);
  font-size: 2.2rem;
  font-weight: 600;
  line-height: 1;
  color: var(--ink-900);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.figure-label {
  font-size: var(--t-xs);
  color: var(--ink-500);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
  margin-top: var(--s-2);
}

/* ── Hero visual: a floating product vignette ────────────────────────────── */

.hero-visual { position: relative; }

.hero-panel {
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-2xl);
  box-shadow: var(--shadow-xl);
  padding: var(--s-5);
  transform: perspective(1400px) rotateY(-7deg) rotateX(3deg);
  transition: transform var(--dur-slow) var(--ease-out);
}

.hero-visual:hover .hero-panel { transform: perspective(1400px) rotateY(-3deg) rotateX(1deg); }

@media (max-width: 1000px) {
  .hero-panel { transform: none; }
  .hero-visual:hover .hero-panel { transform: none; }
}

.hero-panel-head {
  display: flex; align-items: center; gap: var(--s-2);
  padding-bottom: var(--s-4); margin-bottom: var(--s-4);
  border-bottom: 1px solid var(--border);
}

.hero-panel-dots { display: flex; gap: 5px; }
.hero-panel-dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--border-strong); display: block; }

/* Small floating chips around the panel, offset on long slow cycles. */
.float-chip {
  position: absolute;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  font-size: var(--t-sm);
  font-weight: 650;
  white-space: nowrap;
}

.float-chip svg { color: var(--brand-600); }
.float-chip-1 { top: -22px; left: -26px; animation: bob 7s ease-in-out infinite; }
.float-chip-2 { bottom: -20px; right: -18px; animation: bob 8.5s ease-in-out infinite reverse; }

@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } }

@media (max-width: 1000px) { .float-chip { display: none; } }

/* ============================================================================
   13. LANDING SECTIONS
   ========================================================================= */

/* ── Feature cards ───────────────────────────────────────────────────────── */

.feature {
  position: relative;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: var(--s-6);
  overflow: hidden;
  transition: transform var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}

.feature:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); border-color: var(--border-strong); }

/* Radial wash that fades in from the icon corner on hover. */
.feature::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(520px circle at 12% 0%, var(--brand-050), transparent 62%);
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
  pointer-events: none;
}

.feature:hover::before { opacity: 1; }

.feature > * { position: relative; }

.feature-icon {
  width: 52px; height: 52px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-md);
  background: linear-gradient(140deg, var(--brand-100), var(--gold-100));
  border: 1px solid var(--brand-200);
  color: var(--brand-600);
  margin-bottom: var(--s-4);
  transition: transform var(--dur) var(--ease-spring);
}

.feature:hover .feature-icon { transform: scale(1.08) rotate(-4deg); }

.feature h3 { font-size: var(--t-lg); font-family: var(--font-sans); font-weight: 650; margin-bottom: var(--s-2); }
.feature p { color: var(--ink-500); font-size: var(--t-base); margin: 0; }

/* ── Workflow timeline ───────────────────────────────────────────────────── */

.timeline { position: relative; display: grid; gap: var(--s-7); margin: 0; padding: 0; list-style: none; }

/* Connecting spine, drawn behind the markers. */
.timeline::before {
  content: "";
  position: absolute;
  left: 23px; top: 12px; bottom: 12px;
  width: 2px;
  background: linear-gradient(180deg, var(--brand-200), var(--gold-200), transparent);
}

.tl-item { display: grid; grid-template-columns: 48px 1fr; gap: var(--s-5); align-items: start; }

.tl-marker {
  width: 48px; height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--surface-0);
  border: 2px solid var(--brand-200);
  color: var(--brand-600);
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
  position: relative;
  z-index: 1;
  transition: transform var(--dur) var(--ease-spring),
              border-color var(--dur) var(--ease-out),
              background-color var(--dur) var(--ease-out);
}

.tl-item:hover .tl-marker { transform: scale(1.1); border-color: var(--brand-600); background: var(--brand-100); }

.tl-body h3 { font-size: var(--t-lg); font-family: var(--font-sans); font-weight: 650; margin-bottom: var(--s-2); }
.tl-body p { color: var(--ink-500); margin: 0; }

@media (min-width: 900px) {
  .timeline { grid-template-columns: repeat(4, 1fr); gap: var(--s-5); }
  .timeline::before { left: 24px; right: 24px; top: 23px; bottom: auto; width: auto; height: 2px;
                      background: linear-gradient(90deg, var(--brand-200), var(--gold-200), var(--brand-200)); }
  .tl-item { grid-template-columns: 1fr; gap: var(--s-4); }
}

/* ── Testimonials ────────────────────────────────────────────────────────── */

.quote-card {
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

.quote-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }

.quote-mark { font-family: var(--font-display); font-size: 3rem; line-height: 0.6; color: var(--brand-200); }
.quote-text { font-size: var(--t-md); color: var(--ink-700); margin: 0; flex: 1; }

.quote-by { display: flex; align-items: center; gap: var(--s-3); padding-top: var(--s-4); border-top: 1px solid var(--border); }

.quote-avatar {
  width: 42px; height: 42px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(140deg, var(--brand-100), var(--gold-100));
  border: 1px solid var(--brand-200);
  color: var(--brand-600);
  font-weight: 700;
  font-size: var(--t-sm);
  flex: 0 0 auto;
}

.quote-name { font-weight: 650; font-size: var(--t-base); }
.quote-role { font-size: var(--t-sm); color: var(--ink-500); }

/* Placeholder marker. Deliberately visible: this content is illustrative and
   must be replaced with real, attributable quotes before the site is published.
   Making it obvious in the UI is the only reliable way to stop that being
   forgotten — a code comment never survives a copy-paste to production. */
.placeholder-flag {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--warn-100);
  border: 1px dashed var(--warn-600);
  color: var(--warn-700);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* ── Pricing ─────────────────────────────────────────────────────────────── */

.price-grid { display: grid; gap: var(--s-5); grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); align-items: start; }

.price-card {
  position: relative;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}

.price-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }

/* The recommended tier is raised by border and a ribbon, not by a loud fill —
   emphasis should not cost legibility. */
.price-card.is-featured { border-color: var(--brand-600); box-shadow: var(--shadow-lg); }

.price-ribbon {
  position: absolute;
  top: calc(-1 * var(--s-3));
  left: 50%;
  transform: translateX(-50%);
  padding: var(--s-1) var(--s-4);
  border-radius: var(--r-full);
  background: linear-gradient(100deg, var(--brand-600), var(--brand-500));
  color: #fff;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: var(--shadow-sm);
}

.price-name { font-size: var(--t-lg); font-weight: 650; font-family: var(--font-sans); margin: 0; }
.price-amount { font-family: var(--font-display); font-size: 2.6rem; font-weight: 600; line-height: 1; letter-spacing: -0.02em; }
.price-unit { font-size: var(--t-sm); color: var(--ink-500); font-family: var(--font-sans); font-weight: 500; }

.price-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); flex: 1; }
.price-list li { display: flex; gap: var(--s-3); align-items: flex-start; font-size: var(--t-base); color: var(--ink-600); }
.price-list svg { color: var(--ok-600); flex: 0 0 auto; margin-top: 3px; }

/* ── FAQ accordion ───────────────────────────────────────────────────────── */

.faq { display: grid; gap: var(--s-3); }

/* Built on <details>: keyboard operable, screen-reader announced and expandable
   before a single line of JavaScript runs. Re-implementing this with divs and
   click handlers is how accordions become inaccessible. */
.faq-item {
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

.faq-item[open] { border-color: var(--brand-200); box-shadow: var(--shadow-sm); }

.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5);
  cursor: pointer;
  font-weight: 650;
  font-size: var(--t-md);
  list-style: none;
  transition: background-color var(--dur-fast) var(--ease-out);
}

.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary:hover { background: var(--surface-2); }

.faq-chevron {
  flex: 0 0 auto;
  color: var(--brand-600);
  transition: transform var(--dur) var(--ease-out);
}

.faq-item[open] .faq-chevron { transform: rotate(180deg); }

.faq-answer { padding: 0 var(--s-5) var(--s-5); color: var(--ink-600); }
.faq-answer p:last-child { margin-bottom: 0; }

/* ── CTA band ────────────────────────────────────────────────────────────── */

.cta-band {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-2xl);
  padding: var(--s-9) var(--s-7);
  text-align: center;
  background: linear-gradient(135deg, var(--brand-700), var(--brand-600) 55%, var(--brand-500));
  color: #fff;
  box-shadow: var(--shadow-xl);
}

.cta-band h2 { color: #fff; }
.cta-band p { color: rgba(255, 255, 255, 0.86); max-width: 44rem; margin-inline: auto; }

/* Subtle radial texture so the gradient does not read as a flat slab. */
.cta-band::before {
  content: "";
  position: absolute;
  inset: -50%;
  background: radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.16), transparent 45%),
              radial-gradient(circle at 75% 80%, rgba(201, 151, 63, 0.28), transparent 50%);
  pointer-events: none;
}

.cta-band > * { position: relative; }

.cta-band .btn-primary { --btn-bg: #fff; --btn-fg: var(--brand-700); }
.cta-band .btn-primary:hover:not(:disabled) { --btn-bg: var(--brand-050); --btn-fg: var(--brand-800); }
.cta-band .btn-secondary { --btn-bg: transparent; --btn-fg: #fff; --btn-border: rgba(255, 255, 255, 0.5); }
.cta-band .btn-secondary:hover:not(:disabled) { --btn-bg: rgba(255, 255, 255, 0.12); --btn-fg: #fff; --btn-border: #fff; }

/* ============================================================================
   14. FOOTER
   ========================================================================= */

.foot {
  border-top: 1px solid var(--border);
  background: var(--surface-0);
  padding: var(--s-9) 0 var(--s-6);
  font-size: var(--t-base);
  color: var(--ink-500);
  position: relative;
}

.foot::before {
  content: "";
  position: absolute;
  inset: -1px 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--brand-200), var(--gold-200), var(--brand-200), transparent);
}

.foot-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
  gap: var(--s-7);
  padding-bottom: var(--s-7);
  border-bottom: 1px solid var(--border);
}

@media (max-width: 860px) { .foot-grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-6); } }
@media (max-width: 520px) { .foot-grid { grid-template-columns: 1fr; } }

.foot-title {
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-weight: 700;
  color: var(--ink-700);
  margin-bottom: var(--s-4);
}

.foot-links { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.foot-links a { color: var(--ink-500); text-decoration: none; transition: color var(--dur-fast) var(--ease-out); }
.foot-links a:hover { color: var(--brand-600); }

.foot-bottom {
  padding-top: var(--s-5);
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  font-size: var(--t-sm);
}

/* ============================================================================
   15. DOMAIN COMPONENTS
   ========================================================================= */

/* ── Event cards ─────────────────────────────────────────────────────────── */

.event-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}

.event-card:hover,
.event-card:focus-within { transform: translateY(-6px); box-shadow: var(--shadow-lg); border-color: var(--border-strong); }

.event-banner {
  height: 140px;
  display: flex;
  align-items: flex-start;
  padding: var(--s-3) var(--s-4);
  position: relative;
  overflow: hidden;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}

.cat-art {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  color: var(--brand-600);
  transition: transform var(--dur-slow) var(--ease-out);
}

/* Per-category tint, so a listing reads as a set of distinct things rather
   than one shape repeated in one colour. */
.event-banner[data-category="Liturgical"] { background: #f0ecf6; } .event-banner[data-category="Liturgical"] .cat-art { color: #6b4fa0; }
.event-banner[data-category="Feast"]      { background: var(--brand-100); } .event-banner[data-category="Feast"] .cat-art { color: var(--brand-600); }
.event-banner[data-category="Retreat"]    { background: #e8f0ea; } .event-banner[data-category="Retreat"] .cat-art { color: #2f6b4f; }
.event-banner[data-category="Youth"]      { background: #e9f0f6; } .event-banner[data-category="Youth"] .cat-art { color: #2a5578; }
.event-banner[data-category="Family"]     { background: var(--gold-100); } .event-banner[data-category="Family"] .cat-art { color: var(--gold-600); }
.event-banner[data-category="Fundraiser"] { background: #f7ede4; } .event-banner[data-category="Fundraiser"] .cat-art { color: #a8562a; }
.event-banner[data-category="Formation"]  { background: #eeece7; } .event-banner[data-category="Formation"] .cat-art { color: #5d5a4e; }
.event-banner[data-category="Social"]     { background: #f4eaef; } .event-banner[data-category="Social"] .cat-art { color: #8a4468; }
.event-banner[data-category="Picnic"]     { background: #eaf2e6; } .event-banner[data-category="Picnic"] .cat-art { color: #4a7a35; }

:root[data-theme="dark"] .event-banner,
:root[data-theme="dark"] .event-banner[data-category] { background: var(--surface-2); }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .event-banner,
  :root:where(:not([data-theme="light"])) .event-banner[data-category] { background: var(--surface-2); }
}

.event-card:hover .cat-art { transform: scale(1.07); }

/* The organiser's own banner, laid over the category artwork rather than instead of
   it. Same box, same crop, same hover — so a card with a photograph and a card
   without behave identically, and a photograph that 404s uncovers the artwork
   beneath instead of leaving a broken-image box. No z-index: the date chip already
   carries one, which puts it above this. */
.event-banner-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}

.event-card:hover .event-banner-img { transform: scale(1.07); }

.detail-banner { height: 220px; border-radius: var(--r-xl); border: 1px solid var(--border); }
.detail-banner .cat-art { transition: none; }
@media (max-width: 640px) { .detail-banner { height: 160px; } }

.event-date-chip {
  position: relative;
  z-index: 1;
  background: var(--surface-0);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-3);
  text-align: center;
  line-height: 1.05;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease-spring);
}

.event-card:hover .event-date-chip { transform: translateY(-2px) scale(1.04); }

/* THE WEEKDAY TAKES THE MONTH'S OWN TREATMENT — same size, weight, letter-spacing and muted
   ink — so the chip reads as one three-line stack rather than a label stuck above a date. It
   sits first, above the day number, which is the order the date is spoken in. */
.event-date-chip .w { display: block; font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-500); font-weight: 700; }
.event-date-chip .d { display: block; font-size: 1.3rem; font-weight: 700; font-family: var(--font-display); }
.event-date-chip .m { display: block; font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-500); font-weight: 700; }

.event-card-body { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-2); flex: 1 1 auto; }

.event-card-title { font-family: var(--font-display); font-size: var(--t-xl); font-weight: 600; line-height: 1.25; margin: 0; }

.event-card-title a {
  color: var(--ink-900);
  text-decoration: none;
  background-image: linear-gradient(var(--brand-600), var(--brand-600));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1.5px;
  transition: background-size var(--dur-slow) var(--ease-out), color var(--dur) var(--ease-out);
}

.event-card:hover .event-card-title a { background-size: 100% 1.5px; color: var(--brand-600); }

.event-org { font-size: var(--t-sm); color: var(--ink-500); }

/* ── Meta list ───────────────────────────────────────────────────────────
   Icon-and-text lines: when, where, who. Used by the listing card and
   available to any page that needs the same treatment, which is why it is a
   named component rather than a rule scoped to .event-card.

   Each ROW is the flex container. .event-meta was a flex COLUMN whose children
   were therefore blockified, leaving the icon inline inside each row and
   baseline-aligned — 47px of line box for 14px of text. Making the row itself a
   centred flex line is what collapses it to the height of its content.

   The icon is sized by the context rather than by a class on every call site,
   so no markup has to pass a size and no page can forget to. */
.meta-list {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  font-size: var(--t-sm);
  color: var(--ink-600);
}

.meta-item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
}

.meta-item .icon {
  width: 16px;
  height: 16px;
  color: var(--ink-400);
}

/* Long venue names truncate rather than wrapping to a second line and changing
   the card's height — a grid of cards that are all different heights for want of
   one long string reads as broken alignment. The full value stays in the title
   attribute and in the accessibility tree. */
.meta-item > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.event-meta { font-size: var(--t-sm); color: var(--ink-600); display: flex; flex-direction: column; gap: var(--s-1); }

.event-card-foot {
  margin-top: auto;
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
}

.price { font-weight: 700; font-size: var(--t-lg); font-variant-numeric: tabular-nums; }
.price small { font-weight: 500; color: var(--ink-500); font-size: var(--t-xs); display: block; text-transform: uppercase; letter-spacing: 0.06em; }

/* ── Featured carousel ───────────────────────────────────────────────────── */

.carousel { position: relative; }

/* Without JS this is a plain horizontally scrollable strip — every slide still
   reachable, nothing hidden. .is-enhanced switches it to transform-driven. */
.carousel-viewport { overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; border-radius: var(--r-2xl); }
.carousel.is-enhanced .carousel-viewport { overflow: hidden; }

.carousel-track { display: flex; }
.carousel.is-enhanced .carousel-track { transition: transform var(--dur-slow) var(--ease-out); will-change: transform; }

.carousel-slide { flex: 0 0 100%; min-width: 100%; scroll-snap-align: start; padding: 2px; }

.feature-card {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-2xl);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  min-height: 340px;
}

.feature-art { position: relative; overflow: hidden; background: var(--brand-100); }

.feature-art[data-category="Liturgical"] { background: #f0ecf6; } .feature-art[data-category="Liturgical"] .cat-art { color: #6b4fa0; }
.feature-art[data-category="Feast"] .cat-art { color: var(--brand-600); }
.feature-art[data-category="Retreat"] { background: #e8f0ea; } .feature-art[data-category="Retreat"] .cat-art { color: #2f6b4f; }
.feature-art[data-category="Youth"] { background: #e9f0f6; } .feature-art[data-category="Youth"] .cat-art { color: #2a5578; }
.feature-art[data-category="Family"] { background: var(--gold-100); } .feature-art[data-category="Family"] .cat-art { color: var(--gold-600); }
.feature-art[data-category="Fundraiser"] { background: #f7ede4; } .feature-art[data-category="Fundraiser"] .cat-art { color: #a8562a; }
.feature-art[data-category="Formation"] { background: #eeece7; } .feature-art[data-category="Formation"] .cat-art { color: #5d5a4e; }
.feature-art[data-category="Picnic"] { background: #eaf2e6; } .feature-art[data-category="Picnic"] .cat-art { color: #4a7a35; }
.feature-art[data-category="Social"] { background: #f4eaef; } .feature-art[data-category="Social"] .cat-art { color: #8a4468; }

:root[data-theme="dark"] .feature-art { background: var(--surface-2); }

.feature-body { padding: var(--s-6); display: flex; flex-direction: column; gap: var(--s-3); }
.feature-title { font-size: var(--t-2xl); line-height: 1.2; margin: var(--s-1) 0 0; }
.feature-title a { color: var(--ink-900); text-decoration: none; }
.feature-title a:hover { color: var(--brand-600); text-decoration: underline; }
.feature-summary { color: var(--ink-600); margin: 0; }

.feature-meta { display: grid; gap: var(--s-2); margin: var(--s-2) 0 0; }
.feature-meta > div { display: grid; grid-template-columns: 96px 1fr; gap: var(--s-3); }
.feature-meta dt { font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-500); font-weight: 700; padding-top: 3px; }
.feature-meta dd { margin: 0; font-size: var(--t-base); }

.feature-foot {
  margin-top: auto;
  padding-top: var(--s-5);
  border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4); flex-wrap: wrap;
}

.carousel-controls { display: flex; gap: var(--s-2); }

.carousel-btn {
  width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-full);
  border: 1px solid var(--border-strong);
  background: var(--surface-0);
  color: var(--ink-600);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}

.carousel-btn:hover { background: var(--brand-600); border-color: var(--brand-600); color: #fff; transform: scale(1.06); }
:root[data-theme="dark"] .carousel-btn:hover { color: var(--surface-0); }

.carousel-dots { display: flex; justify-content: center; gap: var(--s-2); margin-top: var(--s-5); position: relative; }

.carousel-dot {
  width: 9px; height: 9px; padding: 0;
  border-radius: var(--r-full);
  border: 0;
  background: var(--border-strong);
  cursor: pointer;
  transition: width var(--dur-slow) var(--ease-out), background-color var(--dur) var(--ease-out);
}

/* Current dot stretches into a bar — position readable without relying on
   colour difference alone. */
.carousel-dot.is-current { width: 28px; background: var(--brand-600); }

.carousel-dot::before {
  content: "";
  position: absolute;
  width: 34px; height: 34px;
  transform: translate(-50%, -50%);
}

@media (max-width: 860px) {
  .feature-card { grid-template-columns: 1fr; min-height: 0; }
  .feature-art { height: 160px; }
  .feature-body { padding: var(--s-5); }
  .feature-meta > div { grid-template-columns: 84px 1fr; }
}

/* ── Category rail ───────────────────────────────────────────────────────── */

.rail {
  display: flex;
  gap: var(--s-4);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: var(--s-1) var(--s-1) var(--s-5);
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}

.rail-card {
  flex: 0 0 244px;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}

.rail-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); border-color: var(--border-strong); color: inherit; }

.rail-art { height: 104px; position: relative; overflow: hidden; background: var(--surface-2); }
.rail-card:hover .cat-art { transform: scale(1.08); }

.rail-card[data-category="Liturgical"] .cat-art { color: #6b4fa0; }
.rail-card[data-category="Feast"] .cat-art      { color: var(--brand-600); }
.rail-card[data-category="Retreat"] .cat-art    { color: #2f6b4f; }
.rail-card[data-category="Youth"] .cat-art      { color: #2a5578; }
.rail-card[data-category="Family"] .cat-art     { color: var(--gold-600); }
.rail-card[data-category="Fundraiser"] .cat-art { color: #a8562a; }
.rail-card[data-category="Formation"] .cat-art  { color: #5d5a4e; }
.rail-card[data-category="Social"] .cat-art     { color: #8a4468; }

.rail-body { padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-1); flex: 1; }
.rail-title { font-size: var(--t-base); margin: 0; font-family: var(--font-sans); font-weight: 650; }
.rail-desc { font-size: var(--t-sm); color: var(--ink-500); margin: 0; flex: 1; }
.rail-count { font-size: var(--t-xs); font-weight: 700; color: var(--brand-600); margin-top: var(--s-2); text-transform: uppercase; letter-spacing: 0.06em; }

/* ── Ticket tiers ────────────────────────────────────────────────────────── */

.tier {
  display: flex;
  gap: var(--s-4);
  align-items: center;
  padding: var(--s-4);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
  border-radius: var(--r-sm);
  transition: background-color var(--dur-fast) var(--ease-out);
}

.tier:hover { background: var(--surface-2); }
.tier:last-child { border-bottom: 0; }
.tier-name { font-weight: 650; }
.tier-desc { font-size: var(--t-sm); color: var(--ink-500); }

/* ── Ticket type groups ──────────────────────────────────────────────────
   A ticket type is a heading that owns a stock figure, with its variants as
   rows beneath it. The alternative — every variant repeating the pool's count
   — is four true sentences that add up to a false impression, so the hierarchy
   is carrying real meaning here rather than decorating a list.

   No card, no border box: the heading's weight and the rows' indent are enough
   separation inside a panel that already has an edge, and a nested box inside
   .card-pad reads as a second panel. */
.tier-group + .tier-group { border-top: 1px solid var(--border); }

.tier-group-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  padding-block: var(--s-4) var(--s-2);
}

/* .hint's top margin is for use under a field; on a baseline-aligned row it
   pushes the text off centre. */
.tier-group-from { margin-top: 0; }

/* Indented so the eye can see at a glance which heading a row belongs to, and
   which count therefore governs it. A left rule rather than padding alone: on a
   narrow panel where a row wraps to three lines, indentation on its own stops
   reading as containment. */
/* Scoped under .tier-list to outrank `.tier-list .tier`, which sets the negative
   inline margin that pulls an ungrouped row out to the card's edge. A variant row
   must NOT be pulled out — the whole point is that it sits inside its heading. */
.tier-list .tier-variant {
  margin-inline-start: 0;
  padding-inline-start: var(--s-4);
  border-inline-start: 2px solid var(--border-strong);
}

/* The variant is the row's label, so it carries the weight .tier-name carries on
   an ungrouped row — one step below the heading above it, not two. */
.tier-variant-name { font-weight: 600; }

/* ── Quantity stepper ────────────────────────────────────────────────────
   booking.js wraps the number input in .row.row-tight with a − and a + button.
   Those are .btn-sm at 36px against a 52px input, so the three controls that
   form ONE control sat at two different heights, and 36px is under the 44px
   comfortable-target size.

   Matching them to the input fixes both at once. Scoped to the stepper so
   .btn-sm keeps its size everywhere else. */
.tier .row-tight .btn {
  /* 52px, not 44: that is the .input's own height, and the three controls read as
     one control only if they share an edge. 44 would have cleared the target-size
     guidance while still leaving an 8px step beside the box. */
  min-height: 52px;
  min-width: 52px;
  padding: 0;
  font-size: var(--t-lg);
  line-height: 1;
}

/* .qty-input itself is declared with the form controls in section 8 — see there.
   It is deliberately NOT restated here: a second rule for the same component is
   how two widths end up fighting, which is exactly what happened when this block
   first tried to set one and lost to the original's min-width. */

/* Once .grid-split stacks, the sidebar lands under the form — which on a phone
   means meeting a card field before ever seeing the amount. This modifier lifts
   the summary above it at the stacked width only; side by side, source order
   already reads correctly left to right.

   A modifier rather than a change to .grid-split, because the event page uses
   the same grid and wants its ticket panel where it is. Opt in, per page. */
@media (max-width: 960px) {
  .grid-split.summary-first > :last-child { order: -1; }

  /* align-items must be restated. .grid-split sets `start`, which in a column
     flex container controls the CROSS axis — so the summary shrank to its own
     content width and sat narrower than the form beneath it. */
  .grid-split.summary-first {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }
}

/* ── Booking journey progress ────────────────────────────────────────────
   Four page loads presented as one journey. The checkout previously opened as
   a long form with no indication of how much was left, which is the difference
   between "a guided booking" and "a developer's form".

   Numbers and ticks, not a bar: a bar shows proportion, and what somebody
   filling in a form wants is which named step they are on and what follows.

   State is carried by the marker's shape AND its text (see the .sr-only spans
   in the partial), never by colour alone. */

.journey { margin-bottom: var(--s-6); }

.journey-list {
  display: flex;
  align-items: flex-start;
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: none;
}

.journey-step {
  position: relative;
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  text-align: center;
  min-width: 0;
}

/* The connector. Drawn from each step back towards the previous one, so the
   first step has none and the line always sits behind the markers. */
.journey-step + .journey-step::before {
  content: "";
  position: absolute;
  top: 15px;
  right: 50%;
  left: -50%;
  height: 2px;
  background: var(--border);
  z-index: 0;
}

.journey-step.is-done::before,
.journey-step.is-current::before { background: var(--brand-600); }

.journey-marker {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: var(--r-full);
  border: 2px solid var(--border);
  background: var(--surface-0);
  color: var(--ink-500);
  font-size: var(--t-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out);
}

.journey-marker .icon { width: 16px; height: 16px; }

.journey-step.is-done .journey-marker {
  background: var(--brand-600);
  border-color: var(--brand-600);
  color: #fff;
}

/* The current step is the only ring, which is what makes it findable at a
   glance without reading any of the labels. */
.journey-step.is-current .journey-marker {
  border-color: var(--brand-600);
  color: var(--brand-700);
  box-shadow: 0 0 0 4px var(--brand-100);
}

.journey-label {
  font-size: var(--t-sm);
  color: var(--ink-500);
  line-height: 1.3;
  min-width: 0;
}

.journey-step.is-current .journey-label { color: var(--ink-900); font-weight: 650; }
.journey-step.is-done .journey-label { color: var(--ink-600); }

/* At 375px four labels cannot sit side by side without wrapping to three lines
   each. The markers stay — they are the progress — and the labels reduce to the
   current one, which is the only label that answers "where am I". */
@media (max-width: 560px) {
  .journey-label { font-size: var(--t-xs); }
  .journey-step:not(.is-current) .journey-label { display: none; }
  .journey-step:not(.is-current) { flex: 0 0 auto; }
  .journey-step.is-current { flex: 1 1 auto; }
}

/* ── Order summary ───────────────────────────────────────────────────────
   The panel beside a checkout form. Rows of label/value with one emphasised
   total, and that is the whole component — it is deliberately not a table:
   at 375px a four-column table either scrolls sideways or shrinks the total
   to the size of a footnote, and the total is the one number that has to
   survive. Two elements per row, wrapping independently.

   Everything below is scoped under .summary. Nothing here is a token and
   nothing touches :root, so the landing page is untouched. */

.summary-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-2) 0;

  /* --t-sm, not --t-base. A summary is a list somebody scans for one number,
     not prose they read: at body size a six-line order pushed the total below
     the fold on a phone, and the label and the amount had the same weight as
     the heading above them. Small enough to scan, large enough to trust. */
  font-size: var(--t-sm);
}

/* Secondary rows — "Price each", a per-line GST note. Subordinate to the row
   they qualify, and indented so the eye reads them as belonging to it. */
.summary-row-sub {
  padding-top: 0;
  padding-left: var(--s-3);
  font-size: var(--t-xs);
}

.summary-row-sub dt, .summary-row-sub dd { color: var(--ink-500); font-weight: 500; }

.summary-row dt, .summary-row-label { color: var(--ink-500); margin: 0; }

.summary-row dd, .summary-row-value {
  margin: 0;
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* THE AMOUNT MUST NEVER LEAVE THE SCREEN (issue #156).

   A flex item's minimum width defaults to its content, and the label here holds
   a .tier-chip that refuses to wrap — so a ticket named "General admission —
   Adult (18+ years)" made the label wider than a phone, and the amount beside
   it was pushed past the right edge of the card where nobody could read it.
   Three rules, one cause: the label may shrink, the amount may not, and the
   chip inside a summary row wraps its text rather than dictating the width. */
.summary-row dt, .summary-row-label { min-width: 0; flex: 1 1 auto; }
.summary-row dd, .summary-row-value { flex: 0 0 auto; white-space: nowrap; }
.summary-row .tier-chip { white-space: normal; overflow: visible; text-overflow: clip; }

.summary-rule { border: 0; border-top: 1px solid var(--border); margin: var(--s-3) 0; }

/* The total. Larger and heavier than everything above it, because the amount a
   buyer is about to be charged should not need looking for. */
.summary-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
  padding-top: var(--s-3);
  border-top: 2px solid var(--ink-900);
}

.summary-total-label { font-weight: 650; font-size: var(--t-sm); }

/* --t-xl, down from --t-2xl. The total still has to be the largest thing in the
   panel — that is the whole point of the component — but --t-2xl reaches 1.9rem,
   which on a 375px screen left "No payment required" wrapping to three lines and
   made a four-figure amount wider than the panel holding it.

   The hierarchy is what matters and it is preserved: rows at --t-sm, the label
   at --t-sm, the amount at --t-xl. Roughly a 1.5x step to the number somebody is
   about to be charged, which is ample separation without shouting. */
.summary-total-value {
  font-family: var(--font-display);
  font-size: var(--t-xl);
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

/* ── Success state ───────────────────────────────────────────────────────
   The head of the confirmation page. A completed payment deserves a moment
   that reads as an outcome rather than as one more page of the form.

   The tick is aria-hidden and the heading carries the meaning, so nothing here
   depends on the icon or its colour being perceived. */

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

.success-mark {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  margin: 0 auto var(--s-4);
  border-radius: var(--r-full);
  background: var(--ok-100);
  color: var(--ok-700);
  border: 2px solid var(--ok-600);
}

.success-mark .icon { width: 32px; height: 32px; }

/* One gentle scale-in. Motion is a nicety on a success screen and nothing
   depends on it, so it is dropped entirely for reduced-motion — not merely
   shortened. */
@media (prefers-reduced-motion: no-preference) {
  .success-mark { animation: success-pop var(--dur-slow) var(--ease-spring) both; }

  @keyframes success-pop {
    from { transform: scale(0.6); opacity: 0; }
    to   { transform: scale(1);   opacity: 1; }
  }
}

.success-head .lede { margin-inline: auto; }

/* ── Payment assurance line ──────────────────────────────────────────────
   Sits under the card-details heading. Deliberately a .hint and not a banner:
   it states one true fact (nothing is stored) and makes no claim about
   encryption, compliance or a secure gateway, none of which this deployment
   provides. */
.pay-assurance {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: 0;
}

.pay-assurance .icon { width: 16px; height: 16px; flex: 0 0 auto; }

/* ── Review step ─────────────────────────────────────────────────────────
   Scoped to classes only the review page adds. Every component underneath is
   one the journey already owns — .event-banner from the listing card, .meta-list
   from the same, .summary-total from the payment step, .figure-sm from the event
   page. Nothing shared is redefined. */

/* The event's artwork, sized for a summary rather than a page header. The event
   page's .detail-banner is 220px because there the artwork IS the hero; here it
   identifies a card the buyer scrolls past on the way to a button, and 220px of
   illustration above the amount due buries the amount. */
.review-banner {
  height: 132px;
  border-bottom: 1px solid var(--border);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}

/* Static. .cat-art carries a hover scale for the listing cards, where the whole
   card is a link and the movement answers a pointer. This card is not clickable,
   so the same transform would promise a click that never happens — the rule the
   design system already states for .card-hover. */
.review-banner .cat-art { transition: none; }

@media (max-width: 640px) {
  .review-banner { height: 104px; }
}

/* Date, venue and organiser. .meta-item truncates to one line by design, which is
   right in a grid of listing cards that must stay the same height and wrong here:
   this is the confirmation of what somebody is paying for, and a venue that ends
   in an ellipsis is a venue they cannot check. */
.review-meta { gap: var(--s-3); font-size: var(--t-md); }
.review-meta .meta-item > span { overflow: visible; text-overflow: clip; white-space: normal; }
.review-meta .meta-item { align-items: flex-start; }
.review-meta .meta-item .icon { margin-top: 2px; }

/* Two columns of buyer details, collapsing once they genuinely will not fit.
   .grid-2's auto-fit needs 280px and an email address routinely exceeds it, so
   the pair was breaking to one column on a laptop while the card stayed wide. */
.review-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-5) var(--s-4); }

/* An email is one long unbreakable token; without this it widens the grid track
   and pushes the card into a horizontal scroll on a phone. */
.review-facts .figure-value { overflow-wrap: anywhere; }

@media (max-width: 560px) {
  .review-facts { grid-template-columns: 1fr; }
}

/* ── Attendee list ───────────────────────────────────────────────────────
   Still an <ol>: the names are ordered, one per place. The browser's own marker
   sat in the list's outside gutter — beyond the card's padding — so the names
   hung on an indent that lined up with nothing else on the page.

   The counter is generated, not typed, so the numbering cannot drift from the
   list it labels. */
.attendee-list {
  list-style: none;
  counter-reset: attendee;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.attendee-item {
  counter-increment: attendee;
  display: flex;
  /* Wrapping, so a long ticket badge drops under the name instead of taking the
     name's width — see .attendee-ticket-badge. */
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

/* Same disc as the journey marker, one step down in size, so "place 2 of 3" and
   "step 2 of 4" read as the same idea in the same visual language. */
.attendee-item::before {
  content: counter(attendee);
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-full);
  background: var(--surface-0);
  border: 1px solid var(--border-strong);
  font-size: var(--t-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-600);
}

.attendee-item > span { min-width: 0; overflow-wrap: anywhere; font-weight: 600; }

/* THE NAME IS THE ROW'S REASON TO EXIST AND MUST NOT BE CRUSHED (issue #156).

   With the badge fixed at its full width and the name allowed to shrink to
   nothing, a badge reading "General admission — Adult (18+ years)" left the
   name a column one character wide — "Jino Mathew" rendered as eleven lines.
   The name now grows to fill the row and never starts narrower than ten
   characters; the badge may shrink and wrap, and when the two will not share a
   line it moves to the next one, still on the right. */
.attendee-item > span:not(.attendee-ticket-badge) { flex: 1 1 10ch; }

/* The ticket the place was bought on, pushed to the far edge so a column of
   attendees can be read down the right for "which of these is the child". */
.attendee-ticket-badge {
  margin-left: auto;
  flex: 0 1 auto;
  font-weight: 600;
  white-space: normal;
  text-align: right;
}

/* The same fact on the checkout form, where it sits inside the <label> and so
   must not compete with the field name it qualifies. */
.attendee-ticket {
  margin-left: var(--s-2);
  padding: 2px var(--s-2);
  border-radius: var(--r-full);
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: var(--t-xs);
  font-weight: 650;
  color: var(--ink-600);
  letter-spacing: 0.01em;
}

/* ── Pay panel ───────────────────────────────────────────────────────────
   The amount and the button that agrees to it. Same treatment the event page's
   ticket panel gets — a firmer edge and the next shadow step, no new colour —
   because this is the one card on the page that is about to take money. */
.pay-panel { border-color: var(--border-strong); box-shadow: var(--shadow-md); }

/* .summary-total's 2px rule already separates the figure from what is above it,
   so the card's own .stack margin on top of that reads as a gap rather than a
   division. */
.pay-panel .summary-total { margin-top: var(--s-2); }

/* The actions, given room to be the end of the page rather than one more row.

   align-items:stretch rather than a min-height on the secondary button: .btn-lg's
   rendered height is its min-height plus its own padding and line-height (54 → 58
   as it stands), so any number hard-coded here is right until the button's padding
   changes. Stretching makes the row itself the authority, and the pair reads as one
   control group whatever .btn-lg becomes. */
.pay-actions {
  margin-top: var(--s-5);
  padding-top: var(--s-5);
  border-top: 1px solid var(--border);
  align-items: stretch;
}

/* On a phone the primary action goes full width and leads, with Back beneath it.
   A two-up row at 375px leaves "Continue to payment" about 180px wide and puts
   the destructive-feeling option under the thumb's resting position. */
@media (max-width: 520px) {
  .pay-actions { flex-direction: column-reverse; align-items: stretch; gap: var(--s-3); }
  .pay-actions .btn { width: 100%; }
}

/* ── Booking reference ───────────────────────────────────────────────────
   The one string somebody reads down a phone or shows at the door. It was one
   of six equally-weighted .figure values, which made the single most useful
   thing on the confirmation page exactly as prominent as "Booked".

   Monospace and letter-spaced so characters that look alike are told apart when
   read aloud, and user-select:all so one click selects the whole reference
   rather than half of it. */
.ref-panel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5);
  border: 1px solid var(--brand-200);
  border-radius: var(--r-lg);
  background: var(--brand-100);
}

.ref-panel .stat-label { color: var(--brand-700); }

.ref-code {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-2xl);
  font-weight: 700;
  letter-spacing: 0.14em;
  line-height: 1.1;
  color: var(--ink-900);
  user-select: all;
  -webkit-user-select: all;
  word-break: break-all;
}

/* ── Card entry ──────────────────────────────────────────────────────────
   Grouped inputs for expiry and security code. Two columns down to 360px and
   then one: an expiry field squeezed to 40% of a small phone is a field people
   mistype, and a mistyped expiry reads as a declined card. */

.card-fields { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }

@media (max-width: 360px) {
  .card-fields { grid-template-columns: 1fr; }
}

/* Card and security-code numbers, in a face where 0 and O differ. Letter
   spacing at 0.04em so sixteen digits stay scannable in groups without a
   script reformatting what somebody is halfway through typing. */
.input-card {
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
}

/* The demo banner. Deliberately loud: the one thing worse than a demo till that
   looks fake is one that does not. */
.demo-note {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  padding: var(--s-4);
  border-radius: var(--r-md);
  background: var(--warn-100);
  border: 1px dashed var(--warn-600);
  color: var(--warn-700);
  font-size: var(--t-sm);
}

.demo-note svg { flex: 0 0 auto; margin-top: 2px; }
.demo-note code {
  font-family: var(--font-mono);
  font-weight: 700;
  white-space: nowrap;
}

/* ── QR ticket ───────────────────────────────────────────────────────────── */

.ticket {
  background: var(--surface-0);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-xl);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  max-width: 420px;
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

.ticket:hover { transform: translateY(-3px); box-shadow: var(--shadow-xl); }

.ticket-head { padding: var(--s-5); background: linear-gradient(135deg, var(--brand-700), var(--brand-600)); color: #fff; }
.ticket-head h2 { color: #fff; margin: 0 0 var(--s-1); font-size: var(--t-xl); }
.ticket-head .org { font-size: var(--t-sm); opacity: 0.9; }

.ticket-qr { padding: var(--s-6); text-align: center; background: #fff; border-bottom: 1px dashed var(--border-strong); position: relative; }

/* Perforation notches, so it reads as a physical stub. */
.ticket-qr::before, .ticket-qr::after {
  content: "";
  position: absolute;
  bottom: -11px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--surface-1);
  border: 1px solid var(--border-strong);
}

.ticket-qr::before { left: -12px; }
.ticket-qr::after  { right: -12px; }

/* The QR stays pure black on white in every theme — a tinted or inverted code
   is a code that will not scan. */
.ticket-qr img { width: 232px; height: 232px; image-rendering: pixelated; background: #fff; margin-inline: auto; }

.ticket-body { padding: var(--s-5); display: grid; gap: var(--s-3); }
.ticket-row { display: flex; justify-content: space-between; gap: var(--s-4); font-size: var(--t-base); }
.ticket-row dt { color: var(--ink-500); margin: 0; }
.ticket-row dd { margin: 0; font-weight: 600; text-align: right; }
.ticket-code { font-family: var(--font-mono); font-size: var(--t-md); letter-spacing: 0.1em; word-break: break-all; }

/* ── The booking pass: ONE code for the whole transaction ────────────────── */

/* Centred and given room. This is the single thing on the page somebody holds
   a phone up to, so it is not competing with anything beside it. */
.booking-pass {
  display: grid;
  justify-items: center;
  gap: var(--s-3);
  padding: var(--s-6) var(--s-4);
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  text-align: center;
}

/* Pure black on white in every theme, and never scaled below its natural size.
   A tinted, inverted or resampled code is a code that will not scan — which is
   discovered at the door, by the person holding it. The white plate is explicit
   rather than inherited, so a dark theme cannot invert it. */
.booking-qr {
  /* 345, NOT 228 — see Confirmed.cshtml. 69 modules at 5 css px each.
     228px was 3.3 px per module, and with image-rendering: pixelated below that
     is worse than blur: a 690 -> 228 downscale is a 3.026 ratio, so
     nearest-neighbour sampling drops and merges module columns unevenly and the
     code stops decoding anywhere. 345 is exactly half the source, so every
     module lands on 5 whole pixels. */
  width: 345px;
  height: 345px;
  max-width: 100%;
  background: #fff;
  padding: var(--s-3);
  border-radius: var(--r-md);
  image-rendering: pixelated;
}

/* The instruction, not a caption. It is the sentence the whole panel exists to
   deliver, so it outranks the explanatory hint under it. */
.booking-pass-lead {
  margin: 0;
  font-size: var(--t-md);
  font-weight: 650;
  color: var(--ink-700);
}

/* Who the one code covers. A list, not cards: these people are not each getting
   a pass, they are what the single pass admits. */
.pass-people {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}

.pass-people li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--border);
}

.pass-people li:last-child { border-bottom: 0; }

.pass-name { font-weight: 650; }
.pass-type { color: var(--ink-500); font-size: var(--t-sm); }

/* The ticket number, hard right on its row. Darker than .pass-type: this is a
   value somebody reads out or types into a search box, so it has to be the most
   legible thing on the line after the name. nowrap because a reference broken
   across two lines is one people mis-read. */
.pass-ticket-no {
  color: var(--ink-600);
  font-size: var(--t-sm);
  letter-spacing: 0.04em;
  white-space: nowrap;
  margin-inline-start: auto;
}

/* Three items on a row that used to hold two. On a narrow panel the ticket
   number wraps to its own line rather than squeezing the name to one word per
   line — and the auto margin above keeps it right-aligned either way. */
.pass-people li { flex-wrap: wrap; }

/* Printed, this is what somebody carries. Keep the code whole on one sheet
   rather than letting a page break fall through it. */
@media print {
  .booking-pass { break-inside: avoid; border-color: #000; }
  .booking-qr { padding: 0; }
}

/* ── Public attendance check ─────────────────────────────────────────────── */

/* The page a guest reaches by scanning their own ticket. It extends the
   existing .ticket block rather than forking one, so an entry pass looks like
   an entry pass everywhere in the product. Read once, on a phone, at a door,
   by somebody who has never seen it before. */

.pass-shell { max-width: 460px; margin-inline: auto; display: grid; gap: var(--s-5); }
.pass-shell .ticket { max-width: none; }

.pass-kicker {
  display: block;
  font-size: var(--t-sm);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  opacity: 0.85;
  margin-bottom: var(--s-2);
}

/* A refused pass keeps the pass shape but drops the brand — a maroon banner
   over the word "Not valid" reads as an endorsement at a glance. */
.ticket-head-void { background: linear-gradient(135deg, var(--ink-700, #2a2320), var(--ink-500)); }
.ticket-void { box-shadow: var(--shadow-lg); }

/* The holder block, above the perforation. Deliberately the heaviest thing on
   the pass: it is the line checked against the person standing there. */
.pass-holder {
  padding: var(--s-5);
  background: var(--surface-1);
  border-bottom: 1px dashed var(--border-strong);
  position: relative;
}

/* Perforation notches, matching .ticket-qr, so the pass reads as a stub. */
.pass-holder::before, .pass-holder::after {
  content: "";
  position: absolute;
  bottom: -11px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--surface-1);
  border: 1px solid var(--border-strong);
}

.pass-holder::before { left: -12px; }
.pass-holder::after  { right: -12px; }

.pass-holder-name {
  font-family: var(--font-display);
  font-size: var(--t-2xl, 1.6rem);
  font-weight: 650;
  line-height: 1.15;
  margin-top: var(--s-1);
}

.pass-holder-meta { color: var(--ink-500); font-size: var(--t-sm); margin-top: var(--s-2); }

.pass-label {
  display: block;
  font-size: var(--t-sm);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-500);
  font-weight: 700;
}

.pass-party { padding-top: var(--s-2); border-top: 1px solid var(--border); }

.pass-names { list-style: none; margin: var(--s-3) 0 0; padding: 0; display: grid; gap: var(--s-2); }

.pass-names li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
}

.pass-names .pass-name { font-weight: 650; font-size: var(--t-md); }
.pass-names .pass-type { color: var(--ink-500); font-size: var(--t-sm); white-space: nowrap; }

/* The verdict band. Full bleed at the foot of the pass and always carrying a
   word, never colour alone (UX-3). */
.pass-verdict {
  padding: var(--s-4) var(--s-5);
  text-align: center;
  border-top: 1px solid;
}

.pass-verdict strong { display: block; font-size: var(--t-xl); letter-spacing: 0.02em; }
.pass-verdict span { font-size: var(--t-sm); opacity: 0.9; }
.pass-verdict-note { margin: 0; color: var(--ink-500); }

.pass-verdict-ok   { background: var(--ok-100);     border-color: var(--ok-600);     color: var(--ok-700); }
.pass-verdict-used { background: var(--warn-100);   border-color: var(--warn-600);   color: var(--warn-700); }
.pass-verdict-void { background: var(--danger-100); border-color: var(--danger-600); color: var(--danger-700); }

/* ── Gate scanner ────────────────────────────────────────────────────────── */

.scanner-shell { max-width: 560px; margin-inline: auto; }
.scanner-view { position: relative; background: #000; border-radius: var(--r-xl); overflow: hidden; aspect-ratio: 3 / 4; }
.scanner-view video { width: 100%; height: 100%; object-fit: cover; display: block; }

.scanner-reticle {
  position: absolute;
  inset: 18%;
  border: 3px solid rgba(255, 255, 255, 0.85);
  border-radius: var(--r-lg);
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

/* Full-bleed verdict. A volunteer at a noisy door must read the answer at
   arm's length in under a second, so it takes the whole panel and always
   carries a word as well as a colour. */
.scan-verdict { border-radius: var(--r-xl); padding: var(--s-6) var(--s-5); text-align: center; border: 2px solid; }
.scan-verdict h2 { font-size: var(--t-3xl); margin: 0 0 var(--s-1); color: inherit; }
.scan-verdict p { margin: 0; font-size: var(--t-md); }
.scan-verdict .who { margin-top: var(--s-3); font-size: var(--t-lg); font-weight: 650; }

.scan-admitted  { background: var(--ok-100); border-color: var(--ok-600); color: var(--ok-700); }
.scan-duplicate { background: var(--warn-100); border-color: var(--warn-600); color: var(--warn-700); }
.scan-rejected  { background: var(--danger-100); border-color: var(--danger-600); color: var(--danger-700); }

.scan-log { font-size: var(--t-sm); }
.scan-log li { padding: var(--s-2) 0; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; gap: var(--s-4); }
.scan-log li:last-child { border-bottom: 0; }

.offline-note {
  background: var(--warn-100);
  color: var(--warn-700);
  border: 1px solid var(--warn-600);
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4);
  font-size: var(--t-sm);
  font-weight: 600;
}

/* ── Steps (legacy landing block) ────────────────────────────────────────── */

.steps { list-style: none; margin: var(--s-6) 0 0; padding: 0; display: grid; gap: var(--s-6); grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }
.step { position: relative; }

.step-number {
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px;
  border-radius: 50%;
  background: linear-gradient(140deg, var(--brand-100), var(--gold-100));
  color: var(--brand-700);
  border: 1px solid var(--brand-200);
  font-family: var(--font-display);
  font-size: 1.25rem; font-weight: 600;
  margin-bottom: var(--s-4);
}

.step-title { font-size: var(--t-lg); margin: 0 0 var(--s-2); font-family: var(--font-sans); font-weight: 650; }
.step-text { color: var(--ink-500); margin: 0; }

/* ============================================================================
   16. MOTION
   ========================================================================= */

/* Applied by JS only after IntersectionObserver is confirmed available and the
   visitor has not asked for reduced motion. Without .js-motion, everything is
   simply already in place — an invisible site is far worse than an unanimated one. */
.js-motion .reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--i, 0) * 70ms);
}

.js-motion .reveal.is-visible { opacity: 1; transform: none; }

@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

.js-motion .page-body > .wrap,
.js-motion .page-body > .wrap-narrow { animation: rise var(--dur-slow) var(--ease-out) both; }

.js-motion .hero-line { animation: rise 0.6s var(--ease-out) both; animation-delay: calc(var(--d, 0) * 90ms); }

.alert { animation: rise var(--dur) var(--ease-out) both; }

@keyframes verdict-in { 0% { transform: scale(0.94); opacity: 0; } 60% { transform: scale(1.02); } 100% { transform: scale(1); opacity: 1; } }
.scan-verdict { animation: verdict-in var(--dur) var(--ease-spring) both; }

/* "3 left" pulses four times then stops — emphasis, never the only signal. */
@keyframes pulse-ring {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--warn-600) 40%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--warn-600) 0%, transparent); }
}

.badge-warn.is-scarce { animation: pulse-ring 2.4s var(--ease-out) 4; }

/* ============================================================================
   17. UTILITIES
   ========================================================================= */

.muted { color: var(--ink-500); }
.small { font-size: var(--t-sm); }
.tiny { font-size: var(--t-xs); }
.strong { font-weight: 650; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px; top: 0;
  background: var(--brand-600);
  color: #fff;
  padding: var(--s-3) var(--s-5);
  z-index: 100;
  border-radius: 0 0 var(--r-md) 0;
}

.skip-link:focus { left: 0; color: #fff; }

/* ============================================================================
   18. RESPONSIVE
   ========================================================================= */

@media (max-width: 700px) {
  :root { --nav-h: 64px; }
  body { font-size: var(--t-base); }
  .brand-sub { display: none; }
  .section { padding: var(--s-9) 0; }
  .page-body { padding: var(--s-6) 0 var(--s-9); }
  .hero { padding: var(--s-9) 0 var(--s-8); }
  .card { padding: var(--s-4); }
  .card-lg { padding: var(--s-5); }
  .feature { padding: var(--s-5); }
  .cta-band { padding: var(--s-7) var(--s-5); }
  .hero-figures { gap: var(--s-5); }
  .stat-value { font-size: 1.8rem; }
}

/* ── Wide tables, stacked ────────────────────────────────────────────────────
   Opt-in via .table-stack. One block per row instead of a grid that hides its
   last column.

   WHAT WAS ACTUALLY WRONG, MEASURED AT 375px. The booking receipt is four
   columns. Its min-content width is 357px; the card offers 277px. A table with
   automatic layout cannot be narrower than its min-content width, so
   `.table { width: 100% }` does not apply and .table-scroll does what it is
   for — it scrolls, which puts the TOTAL column 80px out of sight inside the
   card. The amount somebody was charged is the one thing a receipt is for, so
   it is the wrong thing to hide.

   AND IT IS NOT `white-space: nowrap` ON th, WHICH WAS THE FIRST GUESS.
   Letting the headers wrap changes the floor by nothing: TICKET, QUANTITY,
   EACH and TOTAL are single words, and min-content cannot break a word. Of the
   357px, 128px is cell padding (four columns, 32px each) — 46% of the space
   available. There is no combination of padding and font size that fits four
   columns here and stays readable, which is why this stacks instead.

   .pt-table-stack in portal.css is the same technique for the console's queue
   table. It is not shared because the two files are not both loaded: the
   booking journey uses _Layout, which loads site.css alone. This block adds
   what that one has no need of — a tfoot whose rows stay two columns.

   The honest cost is the one that pattern already documents: display:block
   removes the row and column relationships from the accessibility tree, which
   is why every cell carries its heading in data-label. The information stays;
   the grid does not.

   Screen only. The printed receipt has its own rules under .confirm-sheet and a
   narrow sheet must not stack. Every rule is inside the query, so the desktop
   and tablet table is untouched. */
@media screen and (max-width: 560px) {
  .table-stack,
  .table-stack tbody,
  .table-stack tfoot,
  .table-stack tbody tr,
  .table-stack tbody td {
    display: block;
    width: 100%;
  }

  /* The headings move into the cells below, so the row of them is redundant —
     hidden the way .sr-only does it rather than with display:none, which would
     also take it from a screen reader. */
  .table-stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The border between rows becomes the divider between blocks. */
  .table-stack tbody tr {
    padding: var(--s-4) 0;
    border-bottom: 1px solid var(--border);
  }

  .table-stack tbody tr:last-child { border-bottom: 0; }
  .table-stack tbody tr:hover { background: none; }

  .table-stack tbody td {
    padding: 0 0 var(--s-2);
    border-bottom: 0;
  }

  .table-stack tbody td:last-child { padding-bottom: 0; }

  /* A right-aligned figure has nothing to align against once stacked. */
  .table-stack tbody td.num { text-align: left; }

  /* The GST note that rides inside a Ticket cell on the review table. Its own rule right-aligns
     it, which is correct beside a figure in a numeric column and wrong under a left-aligned value
     once stacked — it drifts to the far edge and reads as belonging to nothing. .gst-note-left
     already exists for exactly this choice; this applies it where stacking makes it the right one.
     Only inside the cell, so the note beside the total keeps its alignment. */
  .table-stack tbody td .gst-note { text-align: left; }

  /* The column heading, carried down with its cell. The same treatment
     .table th already uses, so a stacked row reads like the table it came
     from. */
  .table-stack tbody td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: var(--t-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ink-500);
    font-weight: 700;
    margin-bottom: 2px;
  }

  /* ── The totals ──────────────────────────────────────────────────────────
     These do NOT stack. A total is a label and an amount, and the pair is the
     whole point — read down the column, the three amounts have to line up.
     Flex rather than a table row because colspan means nothing here once the
     cells are laid out by hand, and space-between gives the same reading as
     .row-between, which is how this journey already presents an amount paid.

     tfoot ITSELF IS IN THE BLOCK LIST ABOVE, which reads as a contradiction
     and is what makes this work. Left as a table-footer-group inside a table
     that is now display:block, it generates an anonymous table box that
     shrink-wraps to its content: the flex rows came out 144px short of the
     card at 430px and the amounts sat mid-row instead of at the edge —
     measured, after writing this the obvious way first.

     The tint .table th carries comes off: stretched across half a flex row it
     reads as a filled box rather than a heading, and the hairline above each
     row already separates them. */
  .table-stack tfoot tr {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-4);
    border-top: 1px solid var(--border);
  }

  .table-stack tfoot th,
  .table-stack tfoot td {
    padding: var(--s-3) 0;
    border-bottom: 0;
    background: none;
  }

  /* Held to the right edge, so the amounts stack into a column of their own. */
  .table-stack tfoot td.num { text-align: right; }
}

@media print {
  .masthead, .foot, .portal-bar, .btn, .skip-link, .mesh, .carousel-controls, .carousel-dots { display: none !important; }
  body { background: #fff; font-size: 12pt; }
  .ticket { box-shadow: none; border: 1px solid #999; }
  .card, .stat, .event-card { box-shadow: none; border: 1px solid #ccc; }

  /* The confirmation page is the record of a booking until passes are issued, so printing it has
     to produce the booking rather than the website. Anything that only makes sense on screen —
     the progress indicator, breadcrumbs, the action row — comes off. */
  .no-print, .journey, .crumbs { display: none !important; }

  /* Never a page break through the reference or the ticket table: a booking reference split
     across two sheets is a reference nobody can read back. */
  .ref-panel, .table, .success-head { break-inside: avoid; page-break-inside: avoid; }

  /* Ink. The brand wash and the tinted success disc cost a lot of toner and carry no meaning on
     paper, where the heading already says what happened. */
  .ref-panel { background: #fff !important; border: 1px solid #999 !important; }
  .success-mark { display: none !important; }
  .success-head { text-align: left; }

  /* Badges lose their fill but must stay legible — status is text, and on paper it is the only
     way "Confirmed" is conveyed at all. */
  .badge { background: #fff !important; border: 1px solid #999 !important; color: #000 !important; }
}

/* ── The printed booking confirmation ────────────────────────────────────────
   Scoped to .confirm-sheet, which exists only for this. The block above is
   shared by every printable view, so anything general stays there and anything
   that assumes THIS page's structure stays here.

   The screen page is a web page: generous whitespace, a 2.2rem figure value
   that reads well on a phone, cards that float. On A4 those same choices waste
   most of a sheet and push a short booking onto two pages. Everything below is
   presentation only — no content is added, hidden or reordered. */

@media print {

  /* A4 with real margins. Browsers default to whatever the last print dialog
     used, which is how a confirmation ends up clipped at the right edge. */
  @page {
    size: A4;
    margin: 14mm 14mm 16mm;
  }

  .confirm-sheet {
    max-width: none;
    margin: 0;
    font-size: 10.5pt;
    line-height: 1.45;
    color: #000;
  }

  /* ── Rhythm ──────────────────────────────────────────────────────────────
     stack-lg is built for a scrolling page. On paper it is roughly a third of
     a sheet in gaps alone. */
  .confirm-sheet.stack-lg > * + * { margin-top: 7mm; }
  .confirm-sheet .stack > * + * { margin-top: 3mm; }
  .confirm-sheet .card-pad { padding: 5mm; }

  /* ── The heading ─────────────────────────────────────────────────────────
     Kept, because "Payment successful" is the answer to the question somebody
     printed this to have on record. Sized as a document title, not a hero. */
  .confirm-sheet .success-head { margin-bottom: 2mm; }
  .confirm-sheet .success-head h1 { font-size: 17pt; margin: 0 0 1.5mm; }
  .confirm-sheet .lede { font-size: 10.5pt; margin: 0; max-width: none; }

  .confirm-sheet .card-title,
  .confirm-sheet .card-title-lg {
    font-size: 11pt;
    margin: 0 0 3mm;
    padding-bottom: 1.5mm;
    border-bottom: 1px solid #000;
    letter-spacing: 0.02em;
    text-transform: uppercase;
  }

  /* ── The reference ───────────────────────────────────────────────────────
     The one string read aloud at a door. It keeps its prominence on paper. */
  .confirm-sheet .ref-panel {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 6mm;
    padding: 3mm 4mm;
    margin-bottom: 4mm;
  }

  .confirm-sheet .ref-code {
    font-size: 15pt;
    letter-spacing: 0.14em;
  }

  /* ── The figure grid ─────────────────────────────────────────────────────
     Two columns on paper as on screen, but the value drops from 2.2rem to
     something a document uses. At print size the original turns eight short
     facts into most of a page. */
  .confirm-sheet .grid-2 {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3mm 8mm;
  }

  .confirm-sheet .figure { break-inside: avoid; page-break-inside: avoid; }

  .confirm-sheet .figure-value {
    font-size: 11pt;
    font-weight: 650;
    line-height: 1.3;
    display: block;
    margin-top: 0.5mm;
  }

  .confirm-sheet .stat-label {
    font-size: 8pt;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #000;
  }

  /* ── Tables ──────────────────────────────────────────────────────────────
     Ruled rather than tinted: zebra striping costs toner and reads worse in
     greyscale than a hairline does. The scroll container has no meaning on
     paper and would clip. */
  .confirm-sheet .table-scroll { overflow: visible !important; }

  .confirm-sheet .table {
    width: 100%;
    border-collapse: collapse;
    font-size: 10pt;
  }

  .confirm-sheet .table th,
  .confirm-sheet .table td {
    padding: 2mm 3mm;
    border-bottom: 1px solid #999;
    background: #fff !important;
  }

  .confirm-sheet .table th {
    font-size: 8.5pt;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    border-bottom: 1px solid #000;
  }

  /* A header that repeats if a long booking's table does span two sheets, and
     rows that never split down the middle. */
  .confirm-sheet .table thead { display: table-header-group; }
  .confirm-sheet .table tr { break-inside: avoid; page-break-inside: avoid; }

  /* ── Payment ─────────────────────────────────────────────────────────────
     The amount stays the largest thing in its section, because that is what a
     printed receipt is for. */
  .confirm-sheet .price { font-size: 13pt; font-weight: 700; }
  .confirm-sheet .row-between { display: flex; justify-content: space-between; align-items: baseline; gap: 6mm; }

  /* ── The entry pass ──────────────────────────────────────────────────────
     THE QR IS UNTOUCHED IN SIZE AND CONTRAST. It is the one element on the
     sheet that a machine has to read: 228px at print resolution is roughly
     60mm, comfortably above what a phone camera needs, and it must stay pure
     black on white. Only the surrounding whitespace changes. */
  .confirm-sheet .booking-pass {
    break-inside: avoid;
    page-break-inside: avoid;
    text-align: center;
    padding: 4mm 0 0;
  }

  .confirm-sheet .booking-qr {
    padding: 0;
    background: #fff !important;
    image-rendering: pixelated;
  }

  .confirm-sheet .booking-pass-lead {
    font-size: 11pt;
    font-weight: 700;
    margin: 3mm 0 1mm;
  }

  /* ── Attendees ───────────────────────────────────────────────────────────
     Two columns, so a party of six is six lines rather than half a page. */
  .confirm-sheet .pass-people {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5mm 8mm;
    margin: 2mm 0 0;
    padding: 0;
    list-style: none;
  }

  .confirm-sheet .pass-people li {
    display: flex;
    justify-content: space-between;
    gap: 4mm;
    padding: 1mm 0;
    border-bottom: 1px dotted #bbb;
    break-inside: avoid;
  }

  .confirm-sheet .pass-name { font-weight: 650; }
  .confirm-sheet .pass-type { color: #333; }

  /* ── Page breaking ───────────────────────────────────────────────────────
     Sections are kept whole where they can be. A section taller than a sheet
     still has to break somewhere, so this is `avoid` rather than a hard rule —
     forcing it would leave a mostly blank page in front of a long booking. */
  .confirm-sheet > section,
  .confirm-sheet .ticket-qr { break-inside: avoid; page-break-inside: avoid; }

  /* A heading never sits alone at the foot of a sheet. */
  .confirm-sheet h2, .confirm-sheet h3 { break-after: avoid; page-break-after: avoid; }

  /* Never a single line stranded across the fold. */
  .confirm-sheet p, .confirm-sheet li { orphans: 3; widows: 3; }

  /* The hint under the QR and the instructions block are genuinely useful on
     paper — they say what to do at the door — so they stay, just quieter. */
  .confirm-sheet .hint { font-size: 9pt; color: #333; }

  /* Cards become ruled blocks. A shadow prints as a grey smear. */
  .confirm-sheet .card {
    box-shadow: none !important;
    border: 1px solid #999;
    border-radius: 0;
    background: #fff !important;
  }
}

/* ============================================================================
   19. REDUCED MOTION — the single switch that disables the whole motion layer
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /* Critical: animated elements must LAND VISIBLE, not sit at their opening
     frame forever. Vestibular safety must never cost someone the content. */
  .js-motion .reveal { opacity: 1 !important; transform: none !important; }
  .js-motion .hero-line { animation: none !important; opacity: 1 !important; transform: none !important; }

  .mesh span { animation: none !important; }
  .float-chip { animation: none !important; }
  .hero-panel { transform: none !important; }
  .carousel.is-enhanced .carousel-track { transition: none !important; }
  .hero-art { transform: translateY(-50%) !important; }
}

/* ------------------------------------------------------- venue map ---- */

/* The embedded venue map on a public event page. A wrapper with a fixed height
   and an absolutely-filled iframe, because an iframe has no intrinsic aspect
   ratio and would otherwise render at its 300x150 default whatever the column
   width is.

   Placed HERE, in the application section, and deliberately not after the
   landing block below: that ordering is the file's own rule, and .wl- styles
   must stay last so nothing in this section can be overridden by them. */

.event-map {
  position: relative;
  height: 320px;
  margin-top: var(--s-5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
}

.event-map iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}


/* ── Event banner: the page's visual anchor ──────────────────────────────────
   Present on EVERY event, image or not. Without a photograph it draws the
   branded gradient plus the category artwork, so a page with no imagery still
   opens on something designed rather than on a paragraph. */
.ev-banner {
  position: relative;
  isolation: isolate;
  min-height: 420px;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background: linear-gradient(135deg, var(--brand-800), var(--brand-600) 55%, var(--brand-500));
}

.ev-banner-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -2;
}

/* The artwork fallback: oversized, low-opacity and bled off the right edge, so
   it reads as texture behind the words rather than as a picture of a thing. */
.ev-banner-art-inner {
  position: absolute;
  right: -8%;
  top: 50%;
  transform: translateY(-50%);
  width: min(560px, 55%);
  opacity: 0.22;
  z-index: -2;
  pointer-events: none;
}

.ev-banner-art-inner svg { width: 100%; height: auto; }

/* THE SCRIM IS WHAT GUARANTEES CONTRAST. A photograph can be any brightness, so
   the text is never trusted to sit directly on it — this gradient is opaque at
   the bottom, where the words are, and clears towards the top. */
.ev-banner-scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to top,
    rgba(20, 16, 42, 0.88) 0%,
    rgba(20, 16, 42, 0.62) 38%,
    rgba(20, 16, 42, 0.15) 72%,
    rgba(20, 16, 42, 0) 100%);
}

.ev-banner-copy { padding: var(--s-8) 0 var(--s-7); color: #fff; }

.ev-banner-crumbs { color: rgba(255, 255, 255, 0.78); margin-bottom: var(--s-4); }
.ev-banner-crumbs a { color: rgba(255, 255, 255, 0.92); }

.ev-banner-badges { margin-bottom: var(--s-4); }

/* clamp() rather than breakpoints: an event title is content of unknown length,
   and this scales it continuously instead of stepping at two arbitrary widths. */
.ev-banner-title {
  margin: 0 0 var(--s-3);
  font-size: clamp(2rem, 5.2vw, 3.6rem);
  line-height: 1.06;
  letter-spacing: -0.02em;
  font-weight: 700;
  color: #fff;
  text-wrap: balance;
}

.ev-banner-meta {
  margin: 0 0 var(--s-5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  color: rgba(255, 255, 255, 0.86);
  font-size: var(--t-lg);
}

.ev-banner-cta { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.32); }

/* The band below the banner keeps the figures and loses the duplicated title. */
.ev-hero-slim { padding-top: var(--s-6); padding-bottom: var(--s-6); }

/* The ticket panel follows the reader on a wide screen. Capped and scrollable so
   an event with many tiers cannot push its own button off the viewport. */
@media (min-width: 1024px) {
  .ev-sticky {
    position: sticky;
    top: var(--s-5);
    max-height: calc(100vh - var(--s-8));
    overflow-y: auto;
  }
}

/* Motion. Gated behind .js-motion, which _SiteScripts adds only when the browser
   has IntersectionObserver AND the visitor has not asked for reduced motion —
   so this is the existing system, not a second one. */
.js-motion .ev-banner-copy > * {
  animation: ev-rise var(--dur-slow, 520ms) var(--ease-out) both;
}

.js-motion .ev-banner-crumbs { animation-delay: 40ms; }
.js-motion .ev-banner-badges { animation-delay: 90ms; }
.js-motion .ev-banner-title { animation-delay: 140ms; }
.js-motion .ev-banner-meta { animation-delay: 200ms; }
.js-motion .ev-banner-cta { animation-delay: 260ms; }

@keyframes ev-rise {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}

/* A slow drift on the photograph only. Long enough to be felt rather than seen,
   and absent entirely on the artwork fallback, which is already static. */
@media (prefers-reduced-motion: no-preference) {
  .ev-banner-photo .ev-banner-img { animation: ev-drift 24s var(--ease-out) infinite alternate; }
}

@keyframes ev-drift {
  from { transform: scale(1.04); }
  to { transform: scale(1.1); }
}

@media (prefers-reduced-motion: reduce) {
  .js-motion .ev-banner-copy > * { animation: none; }
  .ev-banner-photo .ev-banner-img { animation: none; }
}

@media (max-width: 720px) {
  .ev-banner { min-height: 320px; }
  .ev-banner-copy { padding: var(--s-6) 0 var(--s-5); }
  .ev-banner-meta { font-size: var(--t-md); }
}
/* ============================================================================
   PUBLIC EVENT PAGE
   ----------------------------------------------------------------------------
   The .ev- block. Added for the event page redesign and used only there, so it
   sits with the application components and above the .wl- landing block, per the
   ordering this file documents at the top.

   Everything here reuses the tokens already declared: the same radii, spacing
   scale, borders and shadows as .card and .hero. Nothing new is introduced to
   :root, and no .wl- rule is touched — the landing page is unaffected by every
   rule below.
   ============================================================================ */

/* The banner above the hero. A fixed aspect ratio rather than a height, so the
   space is reserved before the image arrives and the page does not jump — the
   same reason every <img> below carries width and height. */
.ev-banner {
  width: 100%;
  aspect-ratio: 3 / 1;
  max-height: 420px;
  overflow: hidden;
  background: var(--surface-2);
}

.ev-banner img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Portrait phones get a taller crop: 3:1 on a 375px screen is a 125px strip,
   which reads as a divider rather than as the event's own picture. */
@media (max-width: 640px) {
  .ev-banner { aspect-ratio: 16 / 10; }
}

.ev-section { margin-top: var(--s-8); }

/* Layout and motion both come from .ev-strip below. What is left here is what
   is particular to a gallery: how wide one picture is allowed to get.

   auto-fit used to do the layout, and it is what made a one-image gallery a
   full-width band — a single 1fr column has nothing to share the row with, so
   it took all of it. A max-width is the part auto-fit could not express. */
.ev-gallery .ev-strip-item { max-width: 320px; }

.ev-gallery.is-scrolling .ev-strip-item {
  width: clamp(200px, 40vw, 300px);
  max-width: none;
}

/* The item, not the anchor. On the public page each picture is a link to the full
   image; in the portal's preview nothing has been stored yet, so there is nothing
   to link to and the same tile is a span. Both are .ev-strip-item, and styling the
   role rather than the element is what stops the preview drifting away from the
   page it exists to predict. */
.ev-gallery .ev-strip-item {
  display: block;
  border-radius: var(--r-md);
  overflow: hidden;
}

.ev-gallery img {
  width: 100%;
  height: 220px;
  object-fit: cover;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-2);
  display: block;
}

.ev-actions-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: center;
}

.ev-organiser {
  display: flex;
  gap: var(--s-4);
  align-items: flex-start;
  margin-top: var(--s-4);
}

/* The same treatment the portal sidebar gives an organisation, so an organiser
   recognises their own mark across both surfaces. */
.ev-organiser-mark {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border-radius: var(--r-full);
  background: var(--brand-100);
  color: var(--brand-700);
  display: grid;
  place-items: center;
  font-weight: 700;
}

.ev-organiser-name {
  margin: 0 0 var(--s-1);
  font-weight: 650;
  font-size: var(--t-lg);
}

/* As with the gallery: .ev-strip carries the layout, and what stays here is how
   wide one card may become. The card itself — its padding, image, badge and
   type — is untouched by any of this. */
.ev-related .ev-strip-item { max-width: 340px; }

.ev-related.is-scrolling .ev-strip-item {
  width: clamp(240px, 76vw, 320px);
  max-width: none;
}

/* A whole card as one link. text-decoration is cleared here rather than on the
   children so the card never shows a stray underline under its badge. */
.ev-related-card {
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}

.ev-related-card img {
  width: 100%;
  height: 160px;
  object-fit: cover;
  display: block;
  background: var(--surface-2);
}

.ev-related-title {
  margin: var(--s-2) 0 var(--s-1);
  font-weight: 650;
}

/* Lift on hover, and only where a pointer exists — a touch device would show the
   hover state stuck after a tap. Gated on the motion preference for the reason
   every other transition in this file is. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .ev-related-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
  }
}


/* ── Horizontal strips ───────────────────────────────────────────────────────
   The gallery and "what else is on" share one behaviour, so they share one
   implementation: a wrapping row of items at their own size while there are few
   of them, and a seamless marquee once there are enough to be worth moving.

   ONE PIECE OF MARKUP, TWO MODES. Both cases render wrapper > track > copy >
   items and differ only by a class on the wrapper, so the quiet case and the
   moving case cannot drift apart the way two branches of a template would.

   THE GAP IS A TRAILING MARGIN, NOT `gap`, AND THAT IS THE WHOLE TRICK. The loop
   is one animation that translates the track left by exactly one copy's width
   and starts again; it is invisible only if the pixel it lands on is identical
   to the pixel it left, which requires every copy to be exactly the same width.
   Flex `gap` puts space BETWEEN copies and not after the last one, so the copies
   are unequal and the restart lands half a gap out — the small sideways twitch
   that gives every hand-rolled marquee away. A margin after every item makes
   each copy self-contained and identical; the trailing space is never seen,
   because the next copy is sitting in it. */

.ev-strip { margin-top: var(--s-4); }

.ev-strip-copy { display: flex; }

/* Fewer items than the threshold: a plain wrapping row. Items may grow into
   spare space but never past their own max-width, which is what keeps one
   image or one card at its natural size instead of stretched across the page. */
.ev-strip.is-static .ev-strip-copy { flex-wrap: wrap; gap: var(--s-4); }

.ev-strip.is-static .ev-strip-item { flex: 1 1 240px; }

.ev-strip.is-scrolling {
  /* Overridden per strip, inline, from the number of items — see Detail.cshtml
     for why the copy count is computed rather than fixed at two. */
  --mq-copies: 2;
  --mq-duration: 40s;

  position: relative;
  overflow: hidden;

  /* Room for the card's hover lift and its shadow. overflow: hidden clips at the
     padding edge, so without this a card rising 2px would be sheared off at the
     top and its shadow cut square at the bottom. The margin gives the space back
     so the strip still sits where the heading expects it. */
  padding-block: var(--s-2);
  margin-top: calc(var(--s-4) - var(--s-2));

  /* Items arrive and leave rather than being sliced off at a hard edge. A mask
     rather than a gradient overlay, so it works on any background and in either
     theme without a second element to keep in step. */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 5%, #000 95%, transparent 100%);
  mask-image:
    linear-gradient(to right, transparent 0, #000 5%, #000 95%, transparent 100%);
}

.ev-strip.is-scrolling .ev-strip-track {
  display: flex;
  width: max-content;

  /* Transform only. No width, no left, no scrollLeft — nothing that asks the
     browser to lay out or paint a frame, so the whole strip is one composited
     layer moving on the GPU. */
  animation: ev-strip-scroll var(--mq-duration) linear infinite;
  will-change: transform;
}

.ev-strip.is-scrolling .ev-strip-copy { flex: 0 0 auto; }

.ev-strip.is-scrolling .ev-strip-item {
  flex: 0 0 auto;
  margin-right: var(--s-4);
}

@keyframes ev-strip-scroll {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(calc(-100% / var(--mq-copies)), 0, 0); }
}

/* Pause under a pointer: somebody who has stopped to look at a picture should
   not have to chase it. Gated on (hover: hover) because a touch device holds
   the hover state after a tap, which would stop the strip for good. */
@media (hover: hover) {
  .ev-strip.is-scrolling:hover .ev-strip-track { animation-play-state: paused; }
}

/* And whenever a keyboard is inside it, for the same reason — tabbing onto a
   moving link is how a link gets lost. */
.ev-strip.is-scrolling:focus-within .ev-strip-track { animation-play-state: paused; }

/* MOTION IS A PREFERENCE, and continuous movement across the viewport is the
   clearest case in this stylesheet for honouring it. The strip becomes an
   ordinary scrollable row: the animation stops, the mask goes, and the repeated
   copies are removed — with the track no longer moving they would be duplicates
   to swipe past rather than the illusion they exist for. */
@media (prefers-reduced-motion: reduce) {
  .ev-strip.is-scrolling {
    overflow-x: auto;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .ev-strip.is-scrolling .ev-strip-track { animation: none; }

  .ev-strip.is-scrolling .ev-strip-copy[aria-hidden="true"] { display: none; }
}


/* ── The date card ───────────────────────────────────────────────────────────
   Replaces the category artwork that used to sit beside the summary. On the one
   page where a reader's questions are "when" and "where", abstract shapes
   answered neither — this answers both in the shape a calendar already taught
   everybody to read. */
.ev-datecard {
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: var(--s-5);
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

/* The torn-off calendar leaf. The brand gradient rather than a flat fill, and a
   rule under the month so the block reads as a date and not as a logo. */
.ev-datecard-date {
  border-radius: var(--r-lg);
  background: linear-gradient(160deg, var(--brand-600), var(--brand-800));
  color: #fff;
  text-align: center;
  padding: var(--s-4) var(--s-3);
}

.ev-datecard-month {
  display: block;
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.9;
  padding-bottom: var(--s-2);
  border-bottom: 1px solid rgba(255, 255, 255, 0.28);
}

/* Tabular figures so 9 and 30 occupy the same width and the card does not
   change shape between events. */
.ev-datecard-day {
  display: block;
  font-size: 3.4rem;
  line-height: 1.05;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding-top: var(--s-2);
}

.ev-datecard-weekday {
  display: block;
  font-size: var(--t-sm);
  opacity: 0.9;
}

.ev-datecard-date-tbc .ev-datecard-day { font-size: 2.4rem; }

.ev-datecard-facts { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-4); }

.ev-datecard-facts li { display: flex; gap: var(--s-3); align-items: flex-start; }

.ev-datecard-facts .hint { margin-top: 0; }

/* A tinted disc behind each glyph, so the three facts scan as a set rather than
   as three unrelated lines. */
.ev-datecard-icon {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: var(--r-full);
  background: var(--brand-050);
  color: var(--brand-600);
  display: grid;
  place-items: center;
}

.ev-datecard-icon svg { width: 18px; height: 18px; }

.ev-datecard-cta { width: 100%; justify-content: center; }

/* ── Share row ───────────────────────────────────────────────────────────────
   Icons beside the words, not instead of them: an unlabelled glyph is a guess,
   and this row is used once by somebody who has never seen it before. */
.ev-actions-row .btn svg { width: 18px; height: 18px; }

.ev-actions-row .btn {
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .ev-actions-row .btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
}
/* ============================================================================
   20. PUBLIC WEBSITE (/welcome)
   ========================================================================= */

/* The public landing page. Everything here extends the system above rather than
   forming a second one: the sections reuse .wrap, .section, .section-head,
   .eyebrow, .lede, .timeline, .faq, .cta-band, .alert, .reveal and the form
   primitives, and the colour, type, spacing, shadow and easing tokens are the
   ones declared at the top of this file — which is what gives the page a dark
   theme for free.

   Prefixed .wl- so it is obvious which rules belong to the public site and which
   belong to the application. The handful of tokens below are scoped to .wl-body
   rather than :root for the same reason: the landing page wants larger radii and
   a dark canvas, and neither should leak into the portal.

   Placed after the reduced-motion switch above, so anything animated here
   carries its own reduced-motion rule at the end of the section. */

.wl-body {
  --wl-r-sm: 12px;
  --wl-r-md: 16px;
  --wl-r-lg: 22px;
  --wl-r-xl: 28px;
  --wl-r-2xl: 36px;

  /* Hero canvas. Fixed rather than tokenised light/dark: this panel is dark in
     both themes, and a "dark surface" that inverts is not a canvas. */
  --wl-canvas-1: #0f0c22;
  --wl-canvas-2: #171331;
  --wl-canvas-3: #1e1940;

  background: var(--surface-1);
}

/* Filtering hides cards with the hidden attribute, and the UA's [hidden] rule
   loses to any class that sets display. Without this, "hidden" cards stay put. */
[hidden] { display: none !important; }

/* ── Buttons ─────────────────────────────────────────────────────────────── */

.wl-btn {
  --wl-bg: transparent;
  --wl-fg: var(--ink-900);
  --wl-bd: transparent;

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 50px;
  padding: var(--s-3) var(--s-5);
  border-radius: var(--wl-r-sm);
  border: 1px solid var(--wl-bd);
  background: var(--wl-bg);
  color: var(--wl-fg);
  font: inherit;
  font-size: var(--t-base);
  font-weight: 650;
  letter-spacing: -0.005em;
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur) var(--ease-out);
}

.wl-btn:active { transform: translateY(1px); }
.wl-btn-lg { min-height: 58px; padding: var(--s-4) var(--s-6); font-size: var(--t-md); border-radius: var(--wl-r-md); }
/* 44px, not 40: this is the "Register" button on every event card, so it is the
   most-tapped control on the page and it sits inside a dense grid where a miss
   lands on the card behind it. The label still reads small — only the box grew. */
.wl-btn-sm { min-height: 44px; padding: var(--s-2) var(--s-4); font-size: var(--t-sm); border-radius: 10px; }
.wl-btn-block { width: 100%; }

.wl-btn-primary { --wl-bg: var(--brand-600); --wl-fg: #fff; box-shadow: var(--shadow-sm); }
:root[data-theme="dark"] .wl-btn-primary { --wl-fg: var(--surface-0); }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .wl-btn-primary { --wl-fg: var(--surface-0); }
}
.wl-btn-primary:hover { --wl-bg: var(--brand-700); transform: translateY(-2px); box-shadow: var(--shadow-brand); }
:root[data-theme="dark"] .wl-btn-primary:hover { --wl-bg: var(--brand-500); }

/* A single sheen sweep on the primary action only. If every button glints, none
   of them reads as primary. */
.wl-btn-primary::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 -130%;
  width: 55%;
  transform: skewX(-18deg);
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.3), transparent);
  pointer-events: none;
}
.wl-btn-primary:hover::after { transition: left var(--dur-slow) var(--ease-out); left: 150%; }

.wl-btn-ghost { --wl-bg: var(--surface-0); --wl-fg: var(--ink-800); --wl-bd: var(--border-strong); box-shadow: var(--shadow-xs); }
.wl-btn-ghost:hover { --wl-bg: var(--surface-2); --wl-bd: var(--ink-300); transform: translateY(-2px); box-shadow: var(--shadow-sm); }

.wl-arrow { display: inline-flex; transition: transform var(--dur) var(--ease-out); }
.wl-btn:hover .wl-arrow { transform: translateX(4px); }
.wl-arrow svg { width: 19px; height: 19px; }

/* ── Pills ───────────────────────────────────────────────────────────────── */

.wl-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-full);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.6;
  white-space: nowrap;
  border: 1px solid transparent;
}

.wl-pill-brand { background: var(--brand-100); color: var(--brand-700); border-color: var(--brand-200); }
.wl-pill-flat  { background: var(--surface-2); color: var(--ink-600); border-color: var(--border); }
.wl-pill-warn  { background: var(--warn-100); color: var(--warn-700); border-color: color-mix(in srgb, var(--warn-600) 28%, transparent); }
.wl-pill-dot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }

/* ── Scroll progress + fixed bar ─────────────────────────────────────────── */

.wl-progress {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 100%;
  z-index: 120;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--brand-600), var(--gold-500));
  pointer-events: none;
}

/* Two states. Over the dark hero the bar is invisible with light contents; once
   the page scrolls it becomes a frosted light bar. Without that, a dark hero
   would force dark chrome on the entire document. */
.wl-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  border-bottom: 1px solid transparent;
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out);
}

.wl-nav.is-stuck {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom-color: var(--border);
  box-shadow: var(--shadow-sm);
}

.wl-nav-in { display: flex; align-items: center; gap: var(--s-5); min-height: var(--nav-h); }
.wl-nav-wrap { margin-left: auto; }

/* The brand yields before the controls do. With both flex children locked at
   flex:0 0 auto and a 24px gap, the bar needed ~448px of content box on a phone
   and got ~398px — which pushed the hamburger off-screen entirely and sliced the
   CTA in half. Nothing about that was visible in a document-overflow check,
   because .wl-nav is fixed and a fixed element's overflow never reaches
   documentElement.scrollWidth. So the brand now shrinks and truncates: whatever
   else changes, the controls stay on screen and tappable. */
.wl-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  text-decoration: none;
  color: #fff;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  min-height: 44px;
}
.wl-nav.is-stuck .wl-brand { color: var(--ink-900); }
.wl-brand:hover { color: #fff; }
.wl-nav.is-stuck .wl-brand:hover { color: var(--ink-900); }

.wl-mark { width: 42px; height: 42px; flex: 0 0 auto; color: #fff; transition: transform var(--dur-slow) var(--ease-spring); }
.wl-nav.is-stuck .wl-mark { color: var(--brand-600); }
.wl-brand:hover .wl-mark { transform: rotate(-8deg) scale(1.07); }

.wl-brand-txt { display: flex; flex-direction: column; line-height: 1.1; min-width: 0; }
.wl-brand-name { font-family: var(--font-display); font-size: 1.32rem; font-weight: 600; letter-spacing: -0.022em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wl-brand-sub { font-size: 0.62rem; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700; opacity: 0.62;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.wl-links { display: flex; align-items: center; gap: var(--s-1); list-style: none; margin: 0; padding: 0; }

.wl-links a:not(.wl-btn) {
  position: relative;
  display: block;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  text-decoration: none;
  font-size: var(--t-base);
  font-weight: 550;
  white-space: nowrap;
  color: rgba(255, 255, 255, 0.82);
  transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}

.wl-nav.is-stuck .wl-links a:not(.wl-btn) { color: var(--ink-600); }
.wl-links a:not(.wl-btn):hover { color: #fff; background: rgba(255, 255, 255, 0.12); }
.wl-nav.is-stuck .wl-links a:not(.wl-btn):hover { color: var(--ink-900); background: var(--wash); }

.wl-links a:not(.wl-btn)::after {
  content: "";
  position: absolute;
  left: 50%; right: 50%; bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--brand-400), var(--gold-500));
  transition: left var(--dur) var(--ease-out), right var(--dur) var(--ease-out);
}

/* aria-current is a boolean here, not "page": this is a position within one
   document, not a separate page. */
.wl-links a:not(.wl-btn)[aria-current="true"]::after { left: var(--s-3); right: var(--s-3); }
.wl-links a:not(.wl-btn)[aria-current="true"] { color: #fff; }
.wl-nav.is-stuck .wl-links a:not(.wl-btn)[aria-current="true"] { color: var(--brand-600); }

.wl-nav-actions { display: flex; align-items: center; gap: var(--s-2); flex: 0 0 auto; }

.wl-icon-btn {
  width: 44px; height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 11px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}

.wl-nav.is-stuck .wl-icon-btn { border-color: var(--border); background: var(--surface-0); color: var(--ink-600); }
.wl-icon-btn:hover { background: rgba(255, 255, 255, 0.18); transform: translateY(-1px); }
.wl-nav.is-stuck .wl-icon-btn:hover { background: var(--surface-2); color: var(--ink-900); }

.wl-nav-cta { --wl-bg: #fff; --wl-fg: var(--brand-700); }
.wl-nav-cta:hover { --wl-bg: var(--brand-050); --wl-fg: var(--brand-800); transform: translateY(-2px); }
.wl-nav.is-stuck .wl-nav-cta { --wl-bg: var(--brand-600); --wl-fg: #fff; }
.wl-nav.is-stuck .wl-nav-cta:hover { --wl-bg: var(--brand-700); --wl-fg: #fff; }
:root[data-theme="dark"] .wl-nav.is-stuck .wl-nav-cta { --wl-fg: var(--surface-0); }

.wl-burger { display: none; }

/* The panel's own copy of the primary action, revealed only once the bar's copy
   is hidden on the narrowest screens. Exactly one of the two is ever displayed,
   so assistive technology never sees a duplicated "Get started".

   Note the :not(.wl-btn) on every .wl-links a rule above. Without it those rules
   win at (0,1,1) against this one at (0,1,0) whatever the source order, and the
   panel CTA renders as a second "Get started" in the desktop nav row. */
.wl-menu-cta { display: none; }

/* Sign in, beside Get started. Outlined rather than a second filled button: two
   solid buttons side by side read as two primary actions, and only one of them is.

   THE COLOURS ARE STATED TWICE, ONCE PER BACKGROUND, and they have to be. The bar
   is transparent over the dark hero and light glass once stuck, so it is two
   different surfaces wearing one class. currentColor cannot bridge them: .wl-nav
   sets no colour of its own, so it inherits --ink-900 from the body and the label
   comes out dark on the dark hero — invisible, which is exactly what happened.
   The nav's own anchors solve it the same way, at .wl-links a:not(.wl-btn). */
.wl-nav-signin {
  --wl-bg: transparent;
  --wl-fg: #fff;
  --wl-bd: rgba(255, 255, 255, 0.34);
}

/* Matches the hover wash on the neighbouring nav anchors, so the two feel like
   one control strip rather than a link next to a button. */
.wl-nav-signin:hover {
  --wl-bg: rgba(255, 255, 255, 0.14);
  --wl-fg: #fff;
  --wl-bd: rgba(255, 255, 255, 0.55);
  transform: translateY(-2px);
}

/* Stuck: the bar becomes --glass-bg, so the label switches from hard white to the
   ink token and the border to a real one. Tokens, not literals, because --glass-bg
   and --ink-800 both invert with the theme — in dark mode the bar is dark and
   --ink-800 is light, in light mode both flip. A hard-coded colour here would be
   correct in exactly one of the two. */
.wl-nav.is-stuck .wl-nav-signin {
  --wl-fg: var(--ink-800);
  --wl-bd: var(--border-strong);
}

.wl-nav.is-stuck .wl-nav-signin:hover {
  --wl-bg: var(--wash);
  --wl-fg: var(--ink-900);
  --wl-bd: var(--ink-300);
}

/* The panel copy. The panel is --glass-bg, the same surface as the stuck bar, and
   it is never shown over the hero — so it only ever needs the ink pairing, in
   either theme. */
.wl-menu-signin {
  display: none;
  --wl-bg: transparent;
  --wl-fg: var(--ink-800);
  --wl-bd: var(--border-strong);
}

.wl-menu-signin:hover { --wl-bg: var(--wash); --wl-fg: var(--ink-900); }

@media (max-width: 1080px) {
  .wl-burger { display: inline-flex; }
  .wl-nav-wrap { margin-left: auto; }

  .wl-links {
    position: absolute;
    top: 100%; left: 0; right: 0;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-1);
    padding: var(--s-4) var(--s-5) var(--s-5);
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-lg);

    /* Six anchors plus a full-width CTA is taller than a phone held sideways, and
       a panel that runs off the bottom of a fixed header cannot be scrolled to —
       the items below the fold are simply unreachable. dvh over vh so the panel
       does not sit under a mobile browser's collapsing address bar; the vh line
       is the fallback for anything that does not know dvh. overscroll-behavior
       stops a flick inside the panel from scrolling the page behind it. */
    max-height: calc(100vh - var(--nav-h));
    max-height: calc(100dvh - var(--nav-h));
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .wl-links.is-open { display: flex; }
  .wl-links a:not(.wl-btn) { color: var(--ink-700); padding: var(--s-3) var(--s-4); font-size: var(--t-md); }
  .wl-links a:not(.wl-btn)::after { display: none; }
  .wl-links a:not(.wl-btn)[aria-current="true"] { color: var(--brand-600); background: var(--brand-100); }
  .wl-cta-text { display: none; }
}

/* ── Header at phone widths ───────────────────────────────────────────────────
   Three independent guards, because the bar must survive a long brand name, a
   narrow screen and a large system font at the same time:
     1. the strapline goes (the same width the application bar drops .brand-sub)
     2. the gap tightens, returning 12px to the content box
     3. the brand truncates rather than shoving the controls off-screen
   Measured content box at 320px is 288px; the bar needs ~260px after (1)-(3). */

@media (max-width: 700px) {
  .wl-nav-in { gap: var(--s-3); }
  .wl-brand-sub { display: none; }
  .wl-brand { gap: var(--s-2); }
  .wl-mark { width: 38px; height: 38px; }
  .wl-brand-name { font-size: 1.18rem; }
}

/* Below this the bar cannot hold brand + theme + CTA + hamburger without
   crowding the tap targets, so the CTA moves into the panel where it gets a
   full-width row instead of a 40px sliver. */
@media (max-width: 420px) {
  .wl-nav-cta { display: none; }
  .wl-menu-cta { display: inline-flex; width: 100%; margin-top: var(--s-2); }

  /* Sign in moves into the panel with the CTA, for the same reason and at the
     same width — the bar has room for the brand, the theme toggle and the
     hamburger, and nothing else. */
  .wl-nav-signin { display: none; }
  .wl-menu-signin { display: inline-flex; width: 100%; margin-top: var(--s-2); }

  .wl-nav-in { gap: var(--s-2); }
  /* The icon buttons deliberately do NOT shrink here. Dropping them to 40px was
     what bought room for the CTA, and the CTA is gone at this width anyway — so
     the space goes back to the two controls that are left, both of which are the
     only way to reach navigation on a phone. They stay at the 44px minimum. */
}

/* ── Hero ────────────────────────────────────────────────────────────────── */

.wl-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: calc(var(--nav-h) + var(--s-11)) 0 var(--s-12);
  color: #f7f2ef;
  background:
    radial-gradient(1100px 620px at 12% -12%, rgba(108, 43, 217, 0.45), transparent 62%),
    radial-gradient(900px 520px at 88% 4%, rgba(46, 124, 246, 0.24), transparent 58%),
    linear-gradient(175deg, var(--wl-canvas-1) 0%, var(--wl-canvas-2) 55%, var(--wl-canvas-3) 100%);
}

/* A fine grid dissolving downward: structure without a background image. */
.wl-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  opacity: 0.5;
  background-image: linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
                    linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
  background-size: 64px 64px;
  -webkit-mask-image: radial-gradient(120% 90% at 50% 0%, #000 20%, transparent 78%);
          mask-image: radial-gradient(120% 90% at 50% 0%, #000 20%, transparent 78%);
}

.wl-orb { position: absolute; border-radius: 50%; filter: blur(90px); z-index: -1; pointer-events: none; opacity: 0.55; }
.wl-orb-1 { width: 560px; height: 560px; top: -220px; right: -100px;
  background: radial-gradient(circle, rgba(139, 92, 246, 0.7), transparent 68%);
  animation: wl-drift-a 26s ease-in-out infinite alternate; }
.wl-orb-2 { width: 460px; height: 460px; bottom: -200px; left: -120px;
  background: radial-gradient(circle, rgba(201, 151, 63, 0.55), transparent 68%);
  animation: wl-drift-b 32s ease-in-out infinite alternate; }

@keyframes wl-drift-a { to { transform: translate3d(-60px, 50px, 0) scale(1.16); } }
@keyframes wl-drift-b { to { transform: translate3d(70px, -40px, 0) scale(1.12); } }

.wl-hero-grid { display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr); gap: var(--s-10); align-items: center; }

.wl-hero-title { font-size: var(--t-5xl); color: #fff; margin-bottom: var(--s-5); letter-spacing: -0.034em; }
.wl-hero-title span { display: block; }

/* Gradient on the emphasis line only. A fully gradient headline is the most
   over-used effect in the genre and it costs contrast; one line earns it. */
.wl-shine {
  background: linear-gradient(102deg, #f3c9d3 4%, #e9aeba 32%, #e9c78c 74%, #c9973f 98%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .wl-shine { color: #e9aeba; -webkit-text-fill-color: currentColor; }
}

.wl-hero-lede { font-size: clamp(1.05rem, 1rem + 0.35vw, 1.28rem); color: rgba(255, 255, 255, 0.76);
  max-width: 35rem; margin-bottom: var(--s-7); line-height: 1.6; }

.wl-hero-cta { display: flex; gap: var(--s-3); flex-wrap: wrap; margin-bottom: var(--s-7); }
.wl-hero .wl-btn-primary { --wl-bg: #fff; --wl-fg: var(--brand-700); }
.wl-hero .wl-btn-primary:hover { --wl-bg: var(--brand-050); --wl-fg: var(--brand-800); box-shadow: 0 14px 34px -12px rgba(0, 0, 0, 0.6); }
.wl-hero .wl-btn-ghost { --wl-bg: rgba(255, 255, 255, 0.07); --wl-fg: #fff; --wl-bd: rgba(255, 255, 255, 0.26); }
.wl-hero .wl-btn-ghost:hover { --wl-bg: rgba(255, 255, 255, 0.14); --wl-bd: rgba(255, 255, 255, 0.5); }

.wl-announce {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1);
  border-radius: var(--r-full);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.9);
  font-size: var(--t-sm);
  font-weight: 600;
  margin-bottom: var(--s-6);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.wl-announce b { background: #fff; color: var(--brand-700); padding: 0.22rem var(--s-3);
  border-radius: var(--r-full); font-size: var(--t-xs); letter-spacing: 0.04em; text-transform: uppercase; }
.wl-announce span { padding-right: var(--s-3); }

.wl-hero-trust { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-6); list-style: none; margin: 0; padding: 0;
  color: rgba(255, 255, 255, 0.62); font-size: var(--t-sm); font-weight: 550; }
.wl-hero-trust li { display: inline-flex; align-items: center; gap: var(--s-2); }
.wl-hero-trust svg { width: 19px; height: 19px; color: var(--gold-500); }

/* ── Hero product vignette ───────────────────────────────────────────────── */

.wl-stage { position: relative; }

.wl-panel {
  position: relative;
  border-radius: var(--wl-r-2xl);
  padding: var(--s-5);
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.05));
  border: 1px solid rgba(255, 255, 255, 0.16);
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.75);
  backdrop-filter: blur(22px) saturate(1.3);
  -webkit-backdrop-filter: blur(22px) saturate(1.3);
  transform: perspective(1500px) rotateY(-9deg) rotateX(4deg);
  transition: transform var(--dur-slow) var(--ease-out);
}

.wl-stage:hover .wl-panel { transform: perspective(1500px) rotateY(-3deg) rotateX(1deg); }

.wl-panel-bar { display: flex; align-items: center; gap: var(--s-1);
  padding-bottom: var(--s-4); margin-bottom: var(--s-4); border-bottom: 1px solid rgba(255, 255, 255, 0.12); }
.wl-panel-bar i { width: 9px; height: 9px; border-radius: 50%; background: rgba(255, 255, 255, 0.28); display: block; }
.wl-panel-bar span { margin-left: auto; font-size: 0.72rem; letter-spacing: 0.09em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45); font-weight: 700; }

/* The inner card stays light in both themes: it is a picture of the product,
   not a surface of this page. */
.wl-pcard { background: rgba(255, 255, 255, 0.96); border-radius: var(--wl-r-md); padding: var(--s-4); color: #14102a; }
.wl-pcard-top { display: flex; gap: var(--s-4); align-items: center; }
.wl-pcard-top > span:last-child { display: flex; flex-direction: column; gap: 2px; }

.wl-datechip { flex: 0 0 auto; width: 54px; text-align: center; border-radius: 12px; padding: var(--s-1) 0;
  background: linear-gradient(160deg, var(--brand-600), var(--brand-500)); color: #fff; line-height: 1.05;
  box-shadow: 0 6px 14px -6px rgba(108, 43, 217, 0.6); }
:root[data-theme="dark"] .wl-datechip { background: linear-gradient(160deg, #6c2bd9, #7c3aed); }
.wl-datechip b { display: block; font-family: var(--font-display); font-size: 1.4rem; font-weight: 600; }
.wl-datechip i { display: block; font-style: normal; font-size: 0.6rem; letter-spacing: 0.12em; font-weight: 700; opacity: 0.86; }

.wl-pcard-title { font-family: var(--font-display); font-size: 1.12rem; font-weight: 600; line-height: 1.25; }
.wl-pcard-sub { font-size: 0.82rem; color: #686183; }
.wl-pcard-split { display: flex; justify-content: space-between; align-items: center;
  margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid #e7e2f1; }
.wl-pcard-price { font-weight: 750; font-size: 1.05rem; }
.wl-mini-btn { background: #6c2bd9; color: #fff; border-radius: 9px; padding: 0.42rem var(--s-3); font-size: 0.78rem; font-weight: 700; }

.wl-flow { list-style: none; margin: var(--s-4) 0 0; padding: 0; display: grid; gap: var(--s-2); }
.wl-flow li { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-3) var(--s-4); border-radius: 13px;
  background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.1);
  font-size: 0.88rem; font-weight: 600; color: rgba(255, 255, 255, 0.9); }
.wl-flow li em { font-style: normal; margin-left: auto; font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase; color: #8fd6ae; }

.wl-tick { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  background: rgba(124, 195, 156, 0.16); border: 1px solid rgba(124, 195, 156, 0.5); color: #8fd6ae; flex: 0 0 auto; }
.wl-tick svg { width: 15px; height: 15px; }

.wl-chip-float {
  position: absolute;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  background: var(--surface-0);
  color: var(--ink-900);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow-lg);
  font-size: var(--t-sm);
  font-weight: 650;
  white-space: nowrap;
}

.wl-chip-float svg { width: 19px; height: 19px; color: var(--brand-600); }
.wl-chip-a { top: -20px; left: -28px; animation: wl-bob 7s ease-in-out infinite; }
.wl-chip-b { bottom: -22px; right: -16px; animation: wl-bob 8.5s ease-in-out infinite reverse; }

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

@media (max-width: 1000px) {
  .wl-hero { padding: calc(var(--nav-h) + var(--s-9)) 0 var(--s-10); }
  .wl-hero-grid { grid-template-columns: 1fr; gap: var(--s-8); }
  .wl-panel { transform: none; }
  .wl-stage:hover .wl-panel { transform: none; }
  .wl-chip-float { display: none; }
}

/* ── Capability strip ────────────────────────────────────────────────────── */

.wl-strip { border-block: 1px solid var(--border); background: var(--surface-0); padding: var(--s-7) 0; }
.wl-strip-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-5); }
.wl-strip-item { display: flex; flex-direction: column; gap: var(--s-1); padding-left: var(--s-4); border-left: 2px solid var(--brand-200); }

.wl-strip-num {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.4rem + 1.2vw, 2.6rem);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  background: linear-gradient(120deg, var(--brand-600), var(--gold-600));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .wl-strip-num { color: var(--brand-600); -webkit-text-fill-color: currentColor; }
}

.wl-strip-lab { font-size: var(--t-sm); color: var(--ink-500); line-height: 1.5; }
.wl-strip-lab b { display: block; color: var(--ink-800); font-weight: 650; font-size: var(--t-base); }

@media (max-width: 900px) { .wl-strip-grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-6); } }
@media (max-width: 520px) { .wl-strip-grid { grid-template-columns: 1fr; } }

/* ── Problem / solution ──────────────────────────────────────────────────── */

.wl-split { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: var(--s-6); align-items: stretch; }

.wl-pcol {
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--wl-r-xl);
  padding: var(--s-7);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

.wl-pcol:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }

/* The resolved column is raised by an edge and a hairline, not a loud fill — the
   comparison has to stay readable rather than become a poster. */
.wl-pcol.is-win { border-color: var(--brand-200); box-shadow: var(--shadow-md); position: relative; overflow: hidden; }
.wl-pcol.is-win::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: linear-gradient(90deg, var(--brand-600), var(--gold-500)); }

.wl-pcol-head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3);
  padding-bottom: var(--s-5); margin-bottom: var(--s-5); border-bottom: 1px solid var(--border); }

.wl-vs { align-self: center; width: 50px; height: 50px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--surface-0); border: 1px solid var(--border-strong);
  color: var(--brand-600); box-shadow: var(--shadow-sm); flex: 0 0 auto; }

.wl-plist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-5); }
.wl-plist li { display: grid; grid-template-columns: 42px 1fr; gap: var(--s-4); align-items: start; }
.wl-plist li > span:last-child { display: flex; flex-direction: column; gap: 2px; }
.wl-plist b { font-weight: 650; font-size: var(--t-base); }
.wl-plist li > span:last-child > span { color: var(--ink-500); font-size: var(--t-sm); line-height: 1.6; }

.wl-plist-ic { width: 42px; height: 42px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 12px; background: linear-gradient(145deg, var(--brand-100), var(--gold-100));
  border: 1px solid var(--brand-200); color: var(--brand-600); flex: 0 0 auto; }
.wl-plist-ic svg { width: 21px; height: 21px; }

/* The "before" column is stated, not scolded — muted rather than marked with
   error colours. It describes what most organisations do now. */
.wl-plist.is-dull .wl-plist-ic { background: var(--surface-2); border-color: var(--border); color: var(--ink-500); }

@media (max-width: 940px) {
  .wl-split { grid-template-columns: 1fr; }
  .wl-vs { transform: rotate(90deg); }
}

/* ── Feature grid ────────────────────────────────────────────────────────── */

.wl-bento { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--s-5); }
.wl-audiences { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr)); gap: var(--s-5); }

.wl-cell {
  position: relative;
  overflow: hidden;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--wl-r-xl);
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}

.wl-cell::after { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
  background: radial-gradient(560px circle at 15% 0%, var(--brand-050), transparent 62%); }

.wl-cell:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); border-color: var(--border-strong); }
.wl-cell:hover::after { opacity: 1; }
.wl-cell:hover .wl-icon { transform: scale(1.08) rotate(-5deg); }
.wl-cell > * { position: relative; z-index: 1; }
.wl-cell h3 { font-family: var(--font-sans); font-weight: 650; font-size: var(--t-lg); margin: 0; }
.wl-cell p { color: var(--ink-500); font-size: var(--t-base); margin: 0; }

.wl-icon { width: 52px; height: 52px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 15px; background: linear-gradient(145deg, var(--brand-100), var(--gold-100));
  border: 1px solid var(--brand-200); color: var(--brand-600); transition: transform var(--dur) var(--ease-spring); }

/* Spans come from WelcomeFeatureCard.Span, so only the values that model actually
   uses are declared — a .span3 nothing selects is weight for nothing. */
.span2 { grid-column: span 2; }
.span4 { grid-column: span 4; }
.span6 { grid-column: span 6; }

@media (max-width: 1000px) {
  .wl-bento { grid-template-columns: repeat(2, 1fr); }
  .span2, .span4, .span6 { grid-column: span 2; }
}
@media (max-width: 620px) {
  .wl-bento { grid-template-columns: 1fr; }
  .span2, .span4, .span6 { grid-column: span 1; }
}

/* Miniature interfaces. They are what stop the cards reading as an icon set with
   captions — and they are drawn in markup so they theme themselves and cost no
   image request. */
.wl-mini { margin-top: auto; border-radius: var(--wl-r-md); border: 1px solid var(--border); background: var(--surface-2); padding: var(--s-3); overflow: hidden; }

.wl-mini-cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }
.wl-mini-cal i { aspect-ratio: 1; border-radius: 5px; background: var(--surface-0); border: 1px solid var(--border); display: block; }
.wl-mini-cal i.on { background: linear-gradient(150deg, var(--brand-600), var(--brand-500)); border-color: transparent;
  box-shadow: 0 3px 8px -3px rgba(108, 43, 217, 0.55); }
.wl-mini-cal i.dim { opacity: 0.45; }

.wl-mini-rows { display: grid; gap: var(--s-2); }
.wl-mini-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.78rem; color: var(--ink-600); font-weight: 600; }
.wl-mini-row b { font-variant-numeric: tabular-nums; color: var(--ink-900); }
.wl-mini-row.total { border-top: 1px dashed var(--border-strong); padding-top: var(--s-2); color: var(--ink-900); }

.wl-mini-qr { display: grid; grid-template-columns: repeat(9, 1fr); gap: 3px; max-width: 150px; margin-inline: auto; }
.wl-mini-qr i { aspect-ratio: 1; border-radius: 2px; background: var(--border-strong); display: block; }
.wl-mini-qr i.on { background: var(--ink-900); }

.wl-mini-bars { display: flex; align-items: flex-end; gap: 8px; height: 82px; }
.wl-mini-bars i { flex: 1; border-radius: 5px 5px 0 0; background: linear-gradient(180deg, var(--brand-400), var(--brand-600)); display: block; opacity: 0.85; }

.wl-mini-people { display: flex; }
.wl-av { width: 36px; height: 36px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.7rem; font-weight: 750; color: var(--brand-700);
  background: linear-gradient(145deg, var(--brand-100), var(--gold-100));
  border: 2px solid var(--surface-0); margin-left: -10px; }
.wl-av:first-child { margin-left: 0; }

/* ── Product tour ────────────────────────────────────────────────────────── */

.wl-tour { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); gap: var(--s-8); align-items: start; }
.wl-tour-tabs { display: grid; gap: var(--s-3); list-style: none; margin: 0; padding: 0; }

.wl-tour-tab {
  width: 100%;
  display: grid;
  grid-template-columns: 44px 1fr;
  gap: var(--s-4);
  text-align: left;
  align-items: start;
  padding: var(--s-4);
  border-radius: var(--wl-r-md);
  border: 1px solid var(--border);
  background: var(--surface-0);
  color: var(--ink-900);
  font: inherit;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out);
}

.wl-tour-tab:hover { transform: translateX(4px); border-color: var(--brand-200); }
.wl-tour-tab[aria-selected="true"] { border-color: var(--brand-600); box-shadow: var(--shadow-md); }
.wl-tour-tab[aria-selected="true"] .wl-tour-n { background: var(--brand-600); color: #fff; border-color: var(--brand-600); }
:root[data-theme="dark"] .wl-tour-tab[aria-selected="true"] .wl-tour-n { color: var(--surface-0); }

.wl-tour-n { width: 44px; height: 44px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--ink-500);
  font-family: var(--font-display); font-size: 1.05rem; font-weight: 600;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out); }

.wl-tour-tab b { display: block; font-size: var(--t-md); font-weight: 650; margin-bottom: 2px; }
.wl-tour-tab span span { display: block; color: var(--ink-500); font-size: var(--t-sm); line-height: 1.55; }

.wl-tour-stage { position: relative; overflow: hidden; border-radius: var(--wl-r-2xl); border: 1px solid var(--border);
  background: var(--surface-0); box-shadow: var(--shadow-lg); padding: var(--s-7); min-height: 400px; }
.wl-tour-stage::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 200px; pointer-events: none;
  background: radial-gradient(420px circle at 75% 0%, var(--brand-050), transparent 70%); }

.wl-tour-panel { display: none; position: relative; }
.wl-tour-panel.is-on { display: block; }
.js-motion .wl-tour-panel.is-on { animation: wl-fade-up 0.45s var(--ease-out) both; }

@keyframes wl-fade-up { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

.wl-tour-panel .wl-pill { margin-bottom: var(--s-4); }
.wl-tour-panel h3 { font-size: var(--t-2xl); margin-bottom: var(--s-2); }
.wl-tour-panel > p { color: var(--ink-600); margin-bottom: var(--s-5); }

.wl-tour-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.wl-tour-list li { display: grid; grid-template-columns: 22px 1fr; gap: var(--s-3); align-items: start;
  color: var(--ink-600); font-size: var(--t-base); }
.wl-tour-list svg { width: 20px; height: 20px; color: var(--ok-600); margin-top: 3px; }

@media (max-width: 980px) {
  .wl-tour { grid-template-columns: 1fr; gap: var(--s-6); }
  .wl-tour-stage { min-height: 0; }
}

/* ── Event discovery ─────────────────────────────────────────────────────── */

.wl-filters { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-bottom: var(--s-6); }

.wl-filter {
  min-height: 44px;
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-full);
  border: 1px solid var(--border-strong);
  background: var(--surface-0);
  color: var(--ink-600);
  font: inherit;
  font-size: var(--t-sm);
  font-weight: 650;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}

.wl-filter:hover { border-color: var(--brand-400); color: var(--ink-900); transform: translateY(-2px); }
.wl-filter[aria-pressed="true"] { background: var(--brand-600); border-color: var(--brand-600); color: #fff; box-shadow: var(--shadow-brand); }
:root[data-theme="dark"] .wl-filter[aria-pressed="true"] { color: var(--surface-0); }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .wl-filter[aria-pressed="true"] { color: var(--surface-0); }
}

.wl-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr)); gap: var(--s-5); }

.wl-ecard {
  display: flex;
  flex-direction: column;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--wl-r-xl);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
}

.wl-ecard:hover, .wl-ecard:focus-within { transform: translateY(-7px); box-shadow: var(--shadow-lg); border-color: var(--border-strong); }

.wl-ecard-art { height: 150px; position: relative; overflow: hidden; background: var(--surface-2);
  border-bottom: 1px solid var(--border); display: flex; align-items: flex-start; padding: var(--s-3) var(--s-4); }
.wl-ecard:hover .cat-art { transform: scale(1.08); }

/* Per-motif tint, so a set of cards reads as distinct things rather than one
   shape repeated in one colour. */
.wl-ecard-art[data-motif="gather"] { background: var(--brand-100); } .wl-ecard-art[data-motif="gather"] .wl-art { color: var(--brand-600); }
.wl-ecard-art[data-motif="learn"]  { background: #eeece7; }          .wl-ecard-art[data-motif="learn"] .wl-art  { color: #5d5a4e; }
.wl-ecard-art[data-motif="give"]   { background: #f7ede4; }          .wl-ecard-art[data-motif="give"] .wl-art   { color: #a8562a; }
.wl-ecard-art[data-motif="play"]   { background: #e8f0ea; }          .wl-ecard-art[data-motif="play"] .wl-art   { color: #2f6b4f; }
.wl-ecard-art[data-motif="stage"]  { background: #f0ecf6; }          .wl-ecard-art[data-motif="stage"] .wl-art  { color: #6b4fa0; }
.wl-ecard-art[data-motif="family"] { background: var(--gold-100); }  .wl-ecard-art[data-motif="family"] .wl-art { color: var(--gold-600); }

/* Dark theme drops the tints: a pale wash that worked on white becomes a glare
   panel on a dark surface. The motif keeps its own colour. */
:root[data-theme="dark"] .wl-ecard-art,
:root[data-theme="dark"] .wl-ecard-art[data-motif] { background: var(--surface-2); }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .wl-ecard-art,
  :root:where(:not([data-theme="light"])) .wl-ecard-art[data-motif] { background: var(--surface-2); }
}

.wl-ecard-chip { position: relative; z-index: 1; background: var(--surface-0); border: 1px solid var(--border-strong);
  border-radius: 11px; padding: var(--s-1) var(--s-2); text-align: center; line-height: 1.05;
  box-shadow: var(--shadow-sm); transition: transform var(--dur) var(--ease-spring); }
.wl-ecard:hover .wl-ecard-chip { transform: translateY(-2px) scale(1.05); }
.wl-ecard-chip b { display: block; font-family: var(--font-display); font-size: 1.28rem; font-weight: 600; }
.wl-ecard-chip i { display: block; font-style: normal; font-size: 0.6rem; letter-spacing: 0.11em;
  text-transform: uppercase; color: var(--ink-500); font-weight: 700; }

.wl-ecard-body { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-2); flex: 1; }
.wl-ecard-title { font-family: var(--font-display); font-size: 1.24rem; font-weight: 600; line-height: 1.25; margin: 0; }
/* The padding/negative-margin pair grows the tap target from 27px to 45px without
   moving a pixel of the layout: the padding is what a thumb hits, the negative
   margin gives the space straight back to the grid. The underline still tracks
   the text because it is painted from the background edge, not the box edge. */
.wl-ecard-title a { color: var(--ink-900); text-decoration: none;
  display: inline-block; padding-block: 9px; margin-block: -9px;
  background-image: linear-gradient(var(--brand-600), var(--brand-600)); background-repeat: no-repeat;
  background-position: 0 calc(100% - 9px); background-size: 0 1.5px;
  transition: background-size var(--dur-slow) var(--ease-out), color var(--dur) var(--ease-out); }
.wl-ecard:hover .wl-ecard-title a { background-size: 100% 1.5px; color: var(--brand-600); }

.wl-ecard-meta { font-size: var(--t-sm); color: var(--ink-600); display: flex; flex-direction: column; gap: var(--s-1); }
.wl-ecard-meta span { display: inline-flex; align-items: center; gap: var(--s-2); }
.wl-ecard-meta svg { width: 18px; height: 18px; color: var(--ink-400); flex: 0 0 auto; }

.wl-ecard-foot { margin-top: auto; padding-top: var(--s-4); border-top: 1px solid var(--border);
  display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); }

.wl-price { font-weight: 750; font-size: var(--t-lg); font-variant-numeric: tabular-nums; }
.wl-price small { display: block; font-size: var(--t-xs); font-weight: 550; color: var(--ink-500);
  text-transform: uppercase; letter-spacing: 0.07em; }

/* ── Quote carousel ──────────────────────────────────────────────────────── */

.wl-carousel { position: relative; }

/* Without JS this is a plain horizontally scrollable strip — every slide still
   reachable, nothing hidden. .is-enhanced switches it to transform-driven. */
.wl-viewport { overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; border-radius: var(--wl-r-2xl); }
.wl-carousel.is-enhanced .wl-viewport { overflow: hidden; }
.wl-track { display: flex; }
.wl-carousel.is-enhanced .wl-track { transition: transform var(--dur-slow) var(--ease-out); will-change: transform; }
.wl-slide { flex: 0 0 100%; min-width: 100%; scroll-snap-align: start; padding: 3px; }

.wl-quote {
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--wl-r-2xl);
  padding: clamp(1.75rem, 1.2rem + 2vw, 3.25rem);
  box-shadow: var(--shadow-md);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-6);
  align-items: start;
  margin: 0;
}

.wl-quote-mark { font-family: var(--font-display); font-size: 5.5rem; line-height: 0.62;
  background: linear-gradient(150deg, var(--brand-400), var(--gold-500));
  -webkit-background-clip: text; background-clip: text; color: transparent; -webkit-text-fill-color: transparent; }

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .wl-quote-mark { color: var(--brand-400); -webkit-text-fill-color: currentColor; }
}

.wl-quote blockquote { margin: 0 0 var(--s-5); font-family: var(--font-display);
  font-size: clamp(1.2rem, 1.05rem + 0.8vw, 1.7rem); line-height: 1.45; color: var(--ink-800); letter-spacing: -0.012em; }
.wl-quote figcaption { display: flex; align-items: center; gap: var(--s-3); padding-top: var(--s-4); border-top: 1px solid var(--border); }
.wl-quote-av { width: 46px; height: 46px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  background: linear-gradient(145deg, var(--brand-100), var(--gold-100)); border: 1px solid var(--brand-200);
  color: var(--brand-600); font-weight: 750; font-size: var(--t-sm); flex: 0 0 auto; }
.wl-quote-role { font-weight: 650; }
.wl-quote-org { font-size: var(--t-sm); color: var(--ink-500); }

@media (max-width: 700px) {
  .wl-quote { grid-template-columns: 1fr; gap: var(--s-4); }
  .wl-quote-mark { font-size: 3.4rem; }
}

.wl-car-ctrl { display: flex; gap: var(--s-2); }
.wl-car-btn { width: 46px; height: 46px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-full); border: 1px solid var(--border-strong); background: var(--surface-0);
  color: var(--ink-600); cursor: pointer;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out), transform var(--dur) var(--ease-out); }
.wl-car-btn:hover { background: var(--brand-600); border-color: var(--brand-600); color: #fff; transform: scale(1.07); }
:root[data-theme="dark"] .wl-car-btn:hover { color: var(--surface-0); }

/* The dot a thumb hits and the dot an eye sees are two different rectangles. The
   button carries the 44px target (WCAG 2.5.5) and stays transparent; ::before
   draws the 9px pip. Expanding the pip itself would have made the control look
   like a row of buttons, and overlaying an invisible 44px box on a 9px pip 8px
   from its neighbour would have put half of each target inside the next one. */
.wl-dots { display: flex; justify-content: center; gap: 0; margin-top: var(--s-6); }
.wl-dot {
  min-width: 32px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; border: 0; background: none; cursor: pointer;
}
.wl-dot::before {
  content: "";
  width: 9px; height: 9px;
  border-radius: var(--r-full);
  background: var(--border-strong);
  transition: width var(--dur-slow) var(--ease-out), background-color var(--dur) var(--ease-out);
}
/* The current dot stretches into a bar — position readable without relying on a
   colour difference alone. */
.wl-dot.is-current::before { width: 30px; background: var(--brand-600); }

/* ── Pricing ─────────────────────────────────────────────────────────────── */

.wl-tiers { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: var(--s-5); align-items: start; }

.wl-tier { position: relative; background: var(--surface-0); border: 1px solid var(--border);
  border-radius: var(--wl-r-xl); padding: var(--s-7); display: flex; flex-direction: column; gap: var(--s-4);
  transition: transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out); }
.wl-tier:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); }
.wl-tier.is-best { border-color: var(--brand-600); box-shadow: var(--shadow-lg); }

/* Gradient ring, drawn by masking out the card's interior. Behind @supports on
   purpose: without mask compositing the pseudo-element would paint a solid
   gradient over the tier's contents. The brand border above is the fallback and
   already reads as the featured tier. */
@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)) {
  .wl-tier.is-best::before {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    padding: 1px;
    pointer-events: none;
    background: linear-gradient(150deg, var(--brand-600), var(--gold-500));
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask-composite: exclude;
  }
}

.wl-ribbon { position: absolute; top: calc(-1 * var(--s-3)); left: 50%; transform: translateX(-50%);
  padding: var(--s-1) var(--s-4); border-radius: var(--r-full);
  background: linear-gradient(100deg, var(--brand-600), var(--brand-500)); color: #fff;
  font-size: var(--t-xs); font-weight: 750; letter-spacing: 0.06em; text-transform: uppercase;
  white-space: nowrap; box-shadow: var(--shadow-sm); }

.wl-tier-name { font-family: var(--font-sans); font-size: var(--t-lg); font-weight: 650; margin: 0; }
.wl-tier-cost { font-family: var(--font-display); font-size: 2.9rem; font-weight: 600; line-height: 1; letter-spacing: -0.03em; }
.wl-tier-unit { font-size: var(--t-sm); color: var(--ink-500); font-weight: 550; }
.wl-tier ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); flex: 1; }
.wl-tier li { display: grid; grid-template-columns: 20px 1fr; gap: var(--s-3); align-items: start; color: var(--ink-600); font-size: var(--t-base); }
.wl-tier li svg { width: 20px; height: 20px; color: var(--ok-600); margin-top: 3px; }

/* ── Forms ───────────────────────────────────────────────────────────────── */

.wl-form-card { background: var(--surface-0); border: 1px solid var(--border); border-radius: var(--wl-r-2xl);
  padding: clamp(1.5rem, 1rem + 2vw, 2.75rem); box-shadow: var(--shadow-md);
  margin-top: var(--s-9); max-width: 900px; margin-inline: auto; }
.wl-form-card.is-flush { margin-top: 0; max-width: none; }
.wl-form-title { font-size: var(--t-2xl); margin-bottom: var(--s-2); }
.wl-form-card .alert { margin-bottom: var(--s-5); }
.wl-fgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr)); gap: 0 var(--s-5); }
.wl-form-card .field:last-of-type { margin-bottom: var(--s-6); }
.wl-form-card .checkline { align-items: flex-start; }

/* ── Contact ─────────────────────────────────────────────────────────────── */

.wl-contact-grid { display: grid; grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); gap: var(--s-8); align-items: start; }
@media (max-width: 940px) { .wl-contact-grid { grid-template-columns: 1fr; } }

.wl-dl { display: grid; gap: var(--s-5); margin: var(--s-5) 0 0; }
.wl-dl > div { display: grid; gap: var(--s-1); }
.wl-dl dt { display: flex; align-items: center; gap: var(--s-2); font-size: var(--t-xs); text-transform: uppercase;
  letter-spacing: 0.1em; font-weight: 750; color: var(--ink-500); }
.wl-dl dt svg { width: 17px; height: 17px; color: var(--brand-600); }
.wl-dl dd { margin: 0; font-size: var(--t-md); color: var(--ink-800); font-weight: 650; }

/* ── Footer ──────────────────────────────────────────────────────────────── */

.wl-foot { border-top: 1px solid var(--border); background: var(--surface-0); padding: var(--s-9) 0 var(--s-6);
  color: var(--ink-500); position: relative; font-size: var(--t-base); }
.wl-foot::before { content: ""; position: absolute; inset: -1px 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--brand-200), var(--gold-200), var(--brand-200), transparent); }
.wl-foot .wl-brand { color: var(--ink-900); }
.wl-foot .wl-mark { color: var(--brand-600); }

.wl-foot-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) repeat(3, minmax(0, 1fr)); gap: var(--s-7);
  padding-bottom: var(--s-7); border-bottom: 1px solid var(--border); }
@media (max-width: 860px) { .wl-foot-grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-6); } }
@media (max-width: 520px) { .wl-foot-grid { grid-template-columns: 1fr; } }

.wl-foot-t { font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.1em; font-weight: 750;
  color: var(--ink-700); margin-bottom: var(--s-4); }
/* gap drops from 12px to 4px and the links take the difference back as padding:
   the rhythm of the column is unchanged, but each link is now a 25px target
   instead of a 21px one (WCAG 2.5.8) and there is no dead gutter between them. */
.wl-foot-l { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-1); }
.wl-foot-l a { color: var(--ink-500); text-decoration: none; display: inline-block;
  padding-block: 2px; transition: color var(--dur-fast) var(--ease-out); }
.wl-foot-l a:hover { color: var(--brand-600); }

.wl-foot-b { padding-top: var(--s-5); display: flex; justify-content: space-between; gap: var(--s-4);
  flex-wrap: wrap; font-size: var(--t-sm); }
.wl-foot-b span { display: inline-flex; align-items: center; gap: var(--s-2); }
.wl-foot-b svg { width: 18px; height: 18px; flex: 0 0 auto; }

/* ── Responsive ──────────────────────────────────────────────────────────── */

@media (max-width: 700px) {
  .wl-pcol { padding: var(--s-5); }
  .wl-tier { padding: var(--s-6); }
  .wl-tour-stage { padding: var(--s-5); }
  .wl-form-card { margin-top: var(--s-7); }
}

/* ── Reduced motion ──────────────────────────────────────────────────────── */

/* This section sits after the global switch above, so anything it animates has
   to opt out here. */
@media (prefers-reduced-motion: reduce) {
  .wl-orb, .wl-chip-float { animation: none !important; }
  .wl-panel { transform: none !important; }
  .wl-carousel.is-enhanced .wl-track { transition: none !important; }
  .js-motion .wl-tour-panel.is-on { animation: none !important; opacity: 1 !important; transform: none !important; }
  .wl-progress { display: none; }
  .wl-btn:hover, .wl-filter:hover, .wl-cell:hover, .wl-ecard:hover,
  .wl-tier:hover, .wl-tour-tab:hover, .wl-pcol:hover, .wl-car-btn:hover { transform: none !important; }
}

/* The side column: the date card and the ticket panel as one grid child.
   Sticky is applied here rather than to the panel alone, so the whole column
   follows the reader down a long description. */
.ev-side { display: flex; flex-direction: column; gap: var(--s-5); }

@media (min-width: 1024px) {
  .ev-side { position: sticky; top: var(--s-5); align-self: start; }
  .ev-side .ev-sticky { position: static; max-height: none; overflow: visible; }
}

/* ── Event description ───────────────────────────────────────────────────────
   Organiser copy is the longest reading on the page, so it gets measure and
   rhythm rather than the default block spacing. 68ch is the comfortable range
   for continuous prose; wider than that and the eye loses the line it is on. */
.ev-prose { max-width: 68ch; }

.ev-prose p {
  line-height: 1.75;
  color: var(--ink-700);
  margin: 0 0 var(--s-4);
}

.ev-prose p:last-child { margin-bottom: 0; }

/* An opening paragraph carries a little more weight, so the description has a
   lede rather than starting flat. */
.ev-prose p:first-of-type {
  font-size: var(--t-lg);
  color: var(--ink-800);
}

.ev-prose-list {
  margin: 0 0 var(--s-4);
  padding-left: 0;
  list-style: none;
  display: grid;
  gap: var(--s-2);
}

/* A brand-tinted marker instead of a browser bullet, matching the check marks
   used in the pricing lists on the public site. */
.ev-prose-list li {
  position: relative;
  padding-left: var(--s-5);
  line-height: 1.7;
  color: var(--ink-700);
}

.ev-prose-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 7px;
  height: 7px;
  border-radius: var(--r-full);
  background: var(--brand-500);
}

/* ── Event page width ────────────────────────────────────────────────────────
   The shared .wrap caps content at --wrap (1200px), which on a 1800px screen
   leaves ~300px of dead margin either side of an event page whose whole job is
   to look like a promotional landing page.

   SCOPED, NOT GLOBAL. --wrap is used by the frozen landing page in twelve
   places, so widening the token would change a page that is not mine to change.
   This class overrides the width on the event page only, and inherits the same
   gutter and centring so nothing else about .wrap has to be duplicated. */
/* THE DESIGN SYSTEM'S OWN WIDTH, not a wider one.

   This was 1480px, which is what made the page feel empty at the edges: the
   container stretched but the prose inside it is capped at a readable measure,
   so a 1440px screen showed 856px of column holding a 623px paragraph — 233px
   of nothing beside every line. --wrap is 1200px and every other page uses it;
   matching it puts the text back in proportion with the space around it.

   The class is kept because the rules below hang off it as the page's hook. */
/* THE EVENT PAGE RUNS WIDER THAN THE REST OF THE SITE.

   --wrap is 1200px, which on a 1920px screen leaves 360px of gutter each side and
   made the page read as a narrow card adrift in white. This raises the cap for
   this page only 2014 scoped to .ev-wide rather than to --wrap, because .wrap is
   shared with the landing page, whose layout is frozen.

   min() keeps it centred and fluid: below the cap it is the viewport less one
   gutter, above it the cap holds and the margins grow evenly. */
.ev-wide {
  --ev-wrap: 1660px;
  width: min(100% - var(--s-11), var(--ev-wrap));
  margin-inline: auto;
}

/* The split, widened with the container. The content column keeps clearly more
   room than the ticket rail, and the rail is floored so it never collapses to a
   width the quantity steppers cannot sit in. */
/* INSIDE A MIN-WIDTH QUERY, and that is not cosmetic. .grid-split collapses to one
   column under 960px via a max-width query; an unscoped .ev-wide .grid-split rule
   out-specifies it (0,2,0 beats 0,1,0) and the page never stacked on a tablet. */
@media (min-width: 961px) {
  .ev-wide .grid-split { grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); }
}

/* Measure follows the column. 68ch inside a 900px card leaves a third of the card
   empty, which is the same complaint one level down. Still capped 2014 prose that
   runs the full width of a wide screen is unreadable. */
.ev-wide .ev-prose { max-width: 82ch; }

/* Stacked below the split0027s own breakpoint, where a single column wants the
   smaller gutter back. */
@media (max-width: 960px) {
  .ev-wide { width: min(100% - var(--s-6), var(--ev-wrap)); }
}

/* Cards lift slightly under the pointer. Small enough to read as responsiveness
   rather than decoration, and gated like every other motion rule here. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .ev-wide .card {
    transition: transform var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out);
  }

  .ev-wide .card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }

  /* The ticket panel is excluded: it is sticky, and a panel that shifts while
     somebody is aiming at a quantity stepper is a panel that loses the click. */
  .ev-wide .sticky-panel:hover { transform: none; }
}


/* ── Draft preview bar ───────────────────────────────────────────────────────
   Shown only to the organiser previewing their own unpublished event, above the
   banner so it cannot be mistaken for part of the page a visitor would see. */
.ev-preview-bar {
  background: var(--warn-100);
  border-bottom: 1px solid var(--border-strong);
  color: var(--warn-700);
  padding: var(--s-3) 0;
}

.ev-preview-inner {
  display: flex;
  gap: var(--s-4);
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
}

/* ── Rich text editor ────────────────────────────────────────────────────────
   The description field's toolbar and editing surface. Built from the same
   tokens as .input and .card so the editor looks like the rest of the form
   rather than like a component bolted onto it. */
.rte {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-0);
  overflow: hidden;
}

/* Focus is shown on the WRAPPER, not the surface, so the whole control lights
   up the way a single input does. :focus-within is what makes that possible. */
.rte:focus-within {
  border-color: var(--brand-400);
  box-shadow: 0 0 0 3px var(--brand-100);
}

.rte-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1);
  padding: var(--s-2);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border);
}

.rte-btn {
  min-width: 32px;
  height: 32px;
  padding: 0 var(--s-2);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-700);
  font-size: var(--t-sm);
  line-height: 1;
  cursor: pointer;
}

.rte-btn:hover { background: var(--surface-2); border-color: var(--border); }
.rte-btn:active { background: var(--surface-3); }

/* Focus is never removed — a toolbar is reachable by keyboard and the only way
   a keyboard user knows where they are. */
.rte-btn:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 1px;
}

.rte-sep {
  width: 1px;
  align-self: stretch;
  margin: 0 var(--s-1);
  background: var(--border);
}

/* Generous height: this is the longest thing anybody types on the form, and a
   four-line box makes a three-paragraph description feel like a fight. */
.rte-surface {
  min-height: 260px;
  max-height: 60vh;
  overflow-y: auto;
  padding: var(--s-4);
  font-size: var(--t-md);
  line-height: 1.7;
  color: var(--ink-800);
}

.rte-surface:focus { outline: none; }

/* The surface renders what the public page will render, so the same shapes are
   styled here — an organiser writing a heading should see a heading. */
.rte-surface p { margin: 0 0 var(--s-3); }
.rte-surface p:last-child { margin-bottom: 0; }
.rte-surface h2 { font-size: var(--t-xl); margin: var(--s-4) 0 var(--s-2); }
.rte-surface h3 { font-size: var(--t-lg); margin: var(--s-4) 0 var(--s-2); }
.rte-surface ul, .rte-surface ol { margin: 0 0 var(--s-3); padding-left: var(--s-6); }
.rte-surface li { margin-bottom: var(--s-1); }

.rte-surface blockquote {
  margin: 0 0 var(--s-3);
  padding-left: var(--s-4);
  border-left: 3px solid var(--brand-200);
  color: var(--ink-600);
}

.rte-surface a { color: var(--brand-600); }

/* ── Published rich text ─────────────────────────────────────────────────────
   The same shapes on the public page. .ev-prose already sets measure, rhythm and
   the lede treatment; these add the elements the editor can now produce. */
.ev-prose h2 { font-size: var(--t-xl); margin: var(--s-6) 0 var(--s-3); }
.ev-prose h3 { font-size: var(--t-lg); margin: var(--s-5) 0 var(--s-2); }
.ev-prose ul, .ev-prose ol { margin: 0 0 var(--s-4); padding-left: var(--s-6); color: var(--ink-700); }
.ev-prose li { margin-bottom: var(--s-2); line-height: 1.7; }

.ev-prose blockquote {
  margin: 0 0 var(--s-4);
  padding: var(--s-2) 0 var(--s-2) var(--s-5);
  border-left: 3px solid var(--brand-200);
  color: var(--ink-600);
  font-style: italic;
}

.ev-prose a { color: var(--brand-600); text-decoration: underline; }

/* Alignment survives sanitising as an inline style on the block — the only CSS
   property the sanitiser allows through. */
.ev-prose [style*="text-align"] { display: block; }

/* The event countdown, on the banner. Its own line above the meta row so a long
   title, a countdown and a venue do not compete for one line on a phone. */
.ev-countdown { margin: 0 0 var(--s-3); }

/* The "check your inbox" line on a confirmed booking. An .alert-ok laid out as
   icon-beside-text, so the glyph anchors the eye without the words wrapping
   under it on a phone. */
.confirm-delivery {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
}

.confirm-delivery svg { flex: 0 0 auto; width: 22px; height: 22px; margin-top: 2px; }

/* ── GST notes ───────────────────────────────────────────────────────────────
   One class for every "GST Inclusive – 10%" on the site, so the same fact reads
   the same way beside a tier price, a checkout total and a receipt line.

   Deliberately quiet: it qualifies an amount rather than competing with it, and
   an amount with a note the same weight as itself is two things shouting. It is
   still --ink-500 on --surface, which clears 4.5:1. */
.gst-note {
  display: block;
  margin-top: var(--s-1);
  font-size: var(--t-xs);
  font-weight: 500;
  line-height: 1.4;
  color: var(--ink-500);
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}

/* Right-aligned by default, because almost every amount on this site is. The
   modifier is for the places that are not — a table cell already left-aligned,
   or the ticket panel's headline. */
.gst-note { text-align: right; }
.gst-note-left { text-align: left; }

.tier-gst { display: block; }

/* ── Ticket type chips ───────────────────────────────────────────────────────
   A ticket type's name, wherever it appears: the event page, the checkout, the
   receipt, the organiser's own table. Same name, same colour, every time —
   TicketTone derives the tone from the name, so nothing is stored and nothing
   has to be kept in step.

   THE COLOUR IS DECORATION AND NOTHING ELSE. Every chip prints its name, and
   availability, price and status are all still stated in words elsewhere; the
   tone only makes "Adult" and "Adult (concession)" separable at a glance in a
   list. That is also why these are their own tones rather than the status
   palette — an amber ticket type would read as a warning about it.

   Built as a pill on --surface-2 with a coloured dot, rather than a fully
   tinted background. Six saturated pills in a column is a fruit salad; six
   neutral pills with six different dots is a legend. */
.tier-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 2px var(--s-3) 2px var(--s-2);
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink-800);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tier-chip::before {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--tier-tone, var(--ink-400));
}

/* The event page's own tier list, where the name is the heading of the row
   rather than one item in a line. */
.tier-chip-lg { font-size: var(--t-md); font-weight: 650; padding: 3px var(--s-4) 3px var(--s-3); }

/* The "× 2" beside a chip in an order summary. Separate element so it stays
   outside the pill — a quantity is not part of the ticket type's name. */
.tier-chip-qty { margin-left: var(--s-2); color: var(--ink-500); font-variant-numeric: tabular-nums; }

/* The six tones. Hues far enough apart to be told apart at 8px, and each one
   is a colour already in this palette or a neighbour of one — nothing here is
   a new brand colour. */
.tier-chip-1 { --tier-tone: var(--brand-500); }
.tier-chip-2 { --tier-tone: #2a5578; }
.tier-chip-3 { --tier-tone: #2c6b4c; }
.tier-chip-4 { --tier-tone: #8a5c11; }
.tier-chip-5 { --tier-tone: #5c4b8a; }
.tier-chip-6 { --tier-tone: #1f6b6b; }

/* ── Countdown chip ──────────────────────────────────────────────────────────
   "3 days to go", sitting with the start date it counts to. The two answer the
   same question at different speeds, so separating them makes the reader do the
   arithmetic the second one exists to save.

   Two states. Quiet by default, because a countdown that shouts eleven months
   out is one people stop seeing; accented inside a week, which is when "I
   should book" becomes "I should book now". Countdown.IsImminent draws that
   line, not this stylesheet. */
.countdown-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-2);
  padding: 2px var(--s-3);
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink-600);
  font-size: var(--t-xs);
  font-weight: 650;
  line-height: 1.6;
  white-space: nowrap;
}

.countdown-chip svg { width: 13px; height: 13px; flex: 0 0 auto; }

.countdown-chip.is-soon {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent-deep);
}

/* One slow pulse, three times, then still. Long enough to catch the eye on
   arrival and short enough that nothing on the page is still moving while
   somebody reads it. Gated on the motion preference like every other animation
   here — with reduced motion the chip is simply accented and static. */
@media (prefers-reduced-motion: no-preference) {
  .countdown-chip.is-soon { animation: countdown-pulse 2.6s var(--ease-out) 3; }
}

@keyframes countdown-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 34%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 0%, transparent); }
}

/* ── Toasts ──────────────────────────────────────────────────────────────────
   The shared notice, replacing thirteen copies of an inline .alert. Fixed to the
   top-right on a desktop and full-width across the top on a phone, where a
   floating card in a corner is either too small to read or covers the content.

   Built on .alert, so the colour, border and spacing are the ones the design
   system already defines — this adds position and dismissal, nothing else. */
.toast-stack {
  position: fixed;
  top: var(--s-4);
  right: var(--s-4);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  max-width: min(420px, calc(100vw - var(--s-8)));
  pointer-events: none;
}

/* The stack is inert so it never blocks the page behind it; each toast turns
   pointer events back on for its own dismiss button. */
.toast {
  pointer-events: auto;
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  box-shadow: var(--shadow-lg);
  margin: 0;
}

.toast svg { flex: 0 0 auto; width: 20px; height: 20px; margin-top: 2px; }

.toast-text { flex: 1 1 auto; }

.toast-close {
  flex: 0 0 auto;
  background: none;
  border: 0;
  cursor: pointer;
  font-size: var(--t-lg);
  line-height: 1;
  padding: 0 var(--s-1);
  color: inherit;
  opacity: 0.6;
}

.toast-close:hover { opacity: 1; }

/* Entry and exit. Gated on the motion preference like every other transition
   here — with reduced motion the toast simply appears and disappears. */
@media (prefers-reduced-motion: no-preference) {
  .toast { animation: toast-in var(--dur-fast) var(--ease-out) both; }
  .toast.is-leaving { transition: opacity 200ms, transform 200ms; opacity: 0; transform: translateX(12px); }
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}

@media (max-width: 640px) {
  .toast-stack {
    top: var(--s-3);
    left: var(--s-3);
    right: var(--s-3);
    max-width: none;
  }
}

/* ── SmartBooking brand mark + installed-app ergonomics ──────────────────────────────────────
   The mark (_BrandMark.cshtml) fills whatever box wraps it; contexts recolour it through the
   --sb-mark-* custom properties rather than by owning a second copy of the SVG. */
.sb-mark { display: block; width: 100%; height: 100%; }

/* The classic landing nav starts transparent over the hero, where a coloured mark drowns —
   white there, brand colours once the bar is stuck and in the footer. */
.wl-nav .wl-mark { --sb-mark-l: #fff; --sb-mark-r: #fff; --sb-mark-d: #fff; }
.wl-nav.is-stuck .wl-mark,
.wl-foot .wl-mark { --sb-mark-l: #6C2BD9; --sb-mark-r: #2E7CF6; --sb-mark-d: #6C2BD9; }

/* Installed (standalone) display: the page owns the whole screen, including the notch and
   home-indicator regions that viewport-fit=cover exposes. Zero everywhere else. */
@media (display-mode: standalone) {
  .masthead { padding-top: env(safe-area-inset-top, 0px); }
  body { padding-bottom: env(safe-area-inset-bottom, 0px); }
}

/* The native SmartBooking app (TDR-040): the Capacitor shell's WebView is NOT
   display-mode standalone, so the same insets key off html.is-native-app — a class set
   only by pwa.js inside the shell. Left/right cover landscape notches; env() is zero
   everywhere it does not apply, and browsers never carry the class at all. */
html.is-native-app .masthead { padding-top: env(safe-area-inset-top, 0px); }
html.is-native-app body {
  padding-right: env(safe-area-inset-right, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
}

/* No Google sign-in inside the iOS app (App Store guideline 4.8). html.is-native-ios is set only
   by app-auth.js when the Capacitor bridge reports the ios platform; browsers and Android never
   carry it. The hint tells Google registrants to set a password through Forgot password. */
html.is-native-ios [data-google-option] { display: none !important; }
[data-google-ios-hint] { display: none; }
html.is-native-ios [data-google-ios-hint] { display: block; }

/* Touch ergonomics: on coarse pointers every control meets the 44px comfortable-tap floor.
   Pointer-gated so desktop density is untouched. */
@media (pointer: coarse) {
  .btn { min-height: 44px; }
  .icon-btn { width: 44px; height: 44px; }
}
