/* ==========================================================================
   Physiopass GTM Website - Styles

   Redesign (2026-09): repainted from the "Physiopass GTM redesign" Claude
   Design project (Physiopass.dc.html) — a warm off-white canvas, near-black
   ink for headings and primary buttons, a cyan-teal accent used sparingly
   (badges, links, the Pro plan, section indices), Inter throughout, and
   pill-shaped controls.

   What is still genuinely shared with the app, and must not drift from it:
     - the iOS semantic text ramp (label / secondaryLabel) and textOnPrimary
     - the spacing scale (AppSpacing xs..xxxl)

   The site uses Inter for every text role (see --font-family below).

   Everything else is a deliberate marketing-site divergence and is annotated
   at its definition below with the reason. Do not "fix" an annotated token
   towards the app without changing the annotation too.
   ========================================================================== */

/* ---------- Custom Properties ---------- */
:root {
  /* Light only. Tells the UA to paint form controls, scrollbars and the
     canvas under the page in the light appearance, so a visitor with a
     dark system setting does not get inverted chrome or a dark flash
     before first paint. There is no theme toggle. */
  color-scheme: light;

  /* Colors - Primary (cyan-teal accent)
     --color-primary is a TINT, not a fill for white text: white on #009A9B is
     only 3.02:1, below the 4.5:1 WCAG 1.4.3 floor. Use it for borders,
     decorative washes and rgba() tints; any surface that carries white text
     must use --color-primary-dark (6.44:1) or --color-primary-darker
     (13.47:1). --color-primary-darker is also the brand foreground on light
     surfaces (section-index eyebrows); --color-primary-dark is the link/icon
     colour. Values are the sRGB rendering of the design's
     oklch(62%/46%/28%/95% 0.1-0.11 195) accent steps. */
  --color-primary: #009A9B;
  --color-primary-dark: #006869;
  --color-primary-light: #E0F3F3;
  --color-primary-surface: #E0F3F3;
  --color-primary-darker: #003333;
  /* RGB channel triplets for rgba() usage — must mirror the hex tokens above */
  --color-primary-rgb: 0, 154, 155;
  --color-primary-dark-rgb: 0, 104, 105;

  /* Colors - Surfaces
     The redesign is flat: cards are a hairline border on the page's own
     canvas, not a lifted white panel. So --color-surface equals --color-canvas
     — there is no card fill to distinguish. --color-surface-alt is the one
     neutral wash this design does use, for the "chapter" bands that need to
     read as a distinct section (the testimonial panel, alternating feature
     rows, bento cells) — it replaces the old teal-tinted --color-primary-surface
     band; nothing paints a full-bleed section in the brand accent any more.
     --color-surface-variant stays a *local* fill only (skeleton shimmer,
     spinner) — never a section background. --color-ink is the one dark
     surface: headings, primary-button fills, and the icon-dot on
     --color-primary-surface all use it, never a separate "darker" ink. */
  --color-canvas: #FDFCFB;
  --color-surface: #FDFCFB;
  --color-surface-rgb: 253, 252, 251;
  --color-surface-alt: #F6F3F1;
  --color-surface-variant: #EDE8E7;
  --color-ink: #110F0E;
  --color-ink-rgb: 17, 15, 14;
  /* Text ramp on the ink surfaces (primary buttons, the mobile-menu backdrop). */
  --color-text-on-ink: #FDFCFB;
  --color-text-on-ink-secondary: rgba(253, 252, 251, 0.72);
  --color-border-on-ink: rgba(253, 252, 251, 0.14);

  /* Colors - Text
     Three steps, all measured against both surfaces this site paints text on
     (--color-canvas #FDFCFB and --color-surface-alt #F6F3F1):
       --color-text-primary    18.66:1 / 17.30:1  (headings, body)
       --color-text-secondary   7.28:1 /  6.75:1  (subtitles, de-emphasised body)
       --color-text-faint       5.38:1 /  4.99:1  (captions, footer print)
     The design's own "ink-faint" step (oklch 63%) measures 3.43:1 — below the
     4.5:1 body-text floor — and several of its real uses here are actual
     text (trust-strip items, testimonial captions, footer copyright), not
     decoration. --color-text-faint is that step darkened to 52% lightness so
     it still clears AA; --color-decorative-glyph keeps the original 63% value
     for genuinely decorative, informationless glyphs (WCAG 1.4.11-exempt). */
  --color-text-primary: #110F0E;
  --color-text-secondary: #595452;
  --color-text-faint: #6C6866;
  --color-text-faint-rgb: 108, 104, 102;
  --color-text-on-primary: #FDFCFB;
  --color-decorative-glyph: #8C8886;
  --color-text-on-primary-rgb: 253, 252, 251;
  --color-shadow-rgb: 17, 15, 14;

  /* Colors - Borders (the design's single "line" hairline) */
  --color-border: #E1DDDB;
  --color-border-light: #E1DDDB;
  /* Slightly stronger hairline, for hover/emphasis on card surfaces */
  --color-border-strong: #D0CBC8;
  /* Border for controls whose outline is what identifies them as a control
     (the language switcher). WCAG 1.4.11 wants 3:1 for that boundary and
     --color-border is only 1.24:1 on canvas; this darker step clears 3:1. */
  --color-border-control: #8C8886;

  /* The final CTA and the sharing band now sit on the plain canvas, not a
     panel — see .cta__inner / #sharing below — so there is no glow gradient
     left to colour. Kept defined (unused) in case a future panel treatment
     returns; do not resurrect a dark full-bleed band without re-pointing
     these first. */
  --color-cta-start: var(--color-primary-dark);
  --color-cta-end: var(--color-primary-darker);

  /* The one warm accent (orange), reserved for star ratings only — the
     redesign resolves every other accent moment (eyebrows, icon chips, step
     numerals, links, the Pro border) to the brand teal, so amber never
     competes with it as a second "accent" the eye has to reconcile. */
  --color-accent: #F5A623;
  --color-accent-dark: #9A3412;
  --color-accent-surface: #FFF7ED;

  /* Colors - Semantic */
  --color-success: #2E7D32;
  --color-error: #C62828;
  --color-rating: var(--color-accent);

  /* Spacing — the app's AppSpacing scale, 1:1. The app also defines
     xxxxl (64); the site has no 64px inline gap, so it is not mirrored here.
     Vertical section rhythm is a separate scale (--section-y-*, below). */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 12px;
  --space-lg: 16px;
  --space-xl: 24px;
  --space-xxl: 36px;
  --space-xxxl: 48px;

  /* Radius
     DIVERGENCE: this is a softer, wider scale than the app's AppRadius
     (6/10/12/14/18). The app's corners are sized for iOS controls at
     touch-target scale; the site's largest surfaces are marketing cards and
     media panels several times that size, where a 14px corner reads as square.
     Deliberate — do not reduce these to match AppRadius. */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 24px;
  /* Large media frames, the pricing panel and the closing CTA banner. One
     step past --radius-xl so a 600px photo does not read as a card. */
  --radius-2xl: 32px;
  --radius-full: 999px;

  /* Shadows — reserved for elements that genuinely overlay the page
     (popovers, cookie banner, hero media). Card-like surfaces use a hairline
     border on a tinted background instead, matching the app. The fixed navbar
     uses a translucent material plus a scroll-revealed hairline, not a shadow. */
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);

  /* Inter is the site's only typeface. A system fallback keeps text usable if
     the webfont has not loaded or is blocked. The display and index tokens
     remain aliases so existing component rules stay semantic. */
  --font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-display: var(--font-family);
  /* Three type sizes only: metadata, reading/UI copy, and display headings.
     Hierarchy within a tier comes from weight, colour, spacing and layout. */
  --type-meta: 0.875rem;
  --type-body: 1rem;
  --type-display: 2.5rem;
  /* The one documented exception to "three type sizes only": the hero
     headline. It is the page's single display moment and the only place where
     a section-heading size reads as undersized — at >= 80rem the copy column
     is ~750px wide while a 14ch measure at 2.5rem fills barely half of it, and
     the leftover void beside the headline is what makes the still look
     oversized. Nothing but .hero__title may consume this token; every other
     heading stays on --type-display. */
  --type-hero: var(--type-display);
  --font-mono: var(--font-family);

  /* ---------- Breakpoint scale ----------
     The site is MOBILE-FIRST: unprefixed declarations are the small-phone
     layout, and every tier below states only its delta via `min-width`.
     A missing tier therefore degrades to the smaller (safe) layout.

     | Token     | Value   | px @16 | Tier | What lives here                        |
     |-----------|---------|--------|------|----------------------------------------|
     | —         | —       | 0–359  | base | Narrow phones. Unprefixed declarations.|
     | --bp-xs   | 22.5rem | 360    | xs   | Standard phones. Navbar CTA returns.   |
     | --bp-sm   | 30rem   | 480    | sm   | Large phones. Gutter + type only.      |
     | --bp-md   | 48rem   | 768    | md   | Tablet portrait. First multi-col tier. |
     | --bp-lg   | 64rem   | 1024   | lg   | Tablet landscape / small laptop.       |
     | --bp-xl   | 80rem   | 1280   | xl   | Desktop. Width and rhythm open up.     |
     | --bp-xxl  | 100rem  | 1600   | xxl  | Large desktop. Only tier raising width.|

     CSS custom properties cannot be used inside `@media` conditions, and there
     is no build step here for `@custom-media`. These tokens are therefore the
     documented source of truth (and the values any `matchMedia()` in JS must
     agree with); the media queries below repeat the same six literals in range
     syntax, e.g. `@media (width >= 48rem)`. No other value may appear in a
     size-based media query in this file — grep for stray ones.
     Size queries carry layout only; preference queries
     (prefers-reduced-motion / -transparency) stay separate. */
  --bp-xs: 22.5rem;
  --bp-sm: 30rem;
  --bp-md: 48rem;
  --bp-lg: 64rem;
  --bp-xl: 80rem;
  --bp-xxl: 100rem;

  /* Layout — semantic, tier-varying. Overridden inside the tier blocks;
     component rules read these and never mention a breakpoint. */
  --max-width: 1200px;
  --nav-height: 72px;
  /* Horizontal page gutter for every full-bleed section. */
  --gutter: var(--space-lg);
  /* Base vertical section rhythm, tier-varying (see the tier blocks below).
     --section-gap is the raw base; sections never read it directly. They pick
     one of the three steps below, which are fixed multiples of it so their
     relative weight is preserved at every breakpoint — including mobile, where
     the base shrinks rather than the multiplier changing.

       sm — supporting bands that sit between two heavier sections
            (testimonial band, outcome loop)
       md — the default for a full content section
       lg — a section that carries its own tinted background and needs to read
            as a distinct chapter (pro, sharing, section--alt)

     No section may declare a raw vertical padding value. */
  --section-gap: 48px;
  --section-y-sm: calc(var(--section-gap) * 0.7);
  --section-y-md: var(--section-gap);
  --section-y-lg: calc(var(--section-gap) * 1.25);
  /* Vertical breathing room above/below the hero composition. */
  --hero-pad-y: var(--space-xl);
  --hero-media-size: 300px;
  /* Hero QR edge length. 112px, not 72: the audience fork that used to sit
     under the badges is gone, so the codes can use that vertical budget and
     still leave the store-mandated 12px clear space above the 48px badges.
     112² (12544) is larger than a badge's area on purpose — a desktop visitor
     scans these; the badge remains the click target. */
  --hero-qr-size: 112px;
  --feature-image-max: 320px;
  --feature-row-gap: var(--space-xl);
  /* Vertical spacing between feature rows. */
  --features-gap: 48px;
}

/* ---------- Responsive layout variables per tier ----------
   Only the deltas. Anything absent from a tier inherits the smaller one. */
@media (width >= 30rem) {
  :root {
    --section-gap: 56px;
    --hero-pad-y: var(--space-xxl);
    --hero-media-size: 320px;
    --feature-image-max: 360px;
  }
}

@media (width >= 48rem) {
  :root {
    --type-hero: 3rem;
    --gutter: var(--space-xl);
    --section-gap: 64px;
    --hero-pad-y: 56px;
    --hero-media-size: 420px;
    --feature-image-max: 420px;
    --feature-row-gap: var(--space-lg);
    --features-gap: 56px;
  }
}

@media (width >= 64rem) {
  :root {
    --type-hero: 3.25rem;
    --section-gap: 72px;
    --hero-media-size: 440px;
    --feature-image-max: 440px;
    --feature-row-gap: var(--space-xl);
    --features-gap: 64px;
  }
}

@media (width >= 80rem) {
  :root {
    --type-hero: 3.5rem;
    --section-gap: 80px;
    --feature-image-max: 460px;
    --feature-row-gap: var(--space-xxl);
    --features-gap: 80px;
  }
}

@media (width >= 100rem) {
  :root {
    --type-hero: 3.75rem;
    /* The only tier that widens the content band. 1360px, not 1440 — beyond
       ~1360 the hero subtitle and feature copy exceed a comfortable measure.
       Applies to every --max-width consumer at once, by design. */
    --max-width: 1360px;
    --hero-pad-y: 96px;
    /* Hero still is 1792×2400; 500px stays well inside it. */
    --hero-media-size: 500px;
    --feature-image-max: 480px;
  }
}

/* ---------- Increased contrast ----------
   iOS/macOS Accessibility -> Display -> "Increase Contrast" maps to
   `prefers-contrast: more` in Safari (Chrome/Firefox map their equivalent OS
   settings too). The site is light-only, so this is the one increased-contrast
   pass.

   Token overrides only — no selector touches anything, because every text and
   hairline colour in this sheet routes through these custom properties. The
   ramp is preserved (secondary is still lighter than faint, border-light is
   still lighter than border); each step is just pushed until it clears the
   floor on BOTH surfaces the site paints on, --color-canvas (#FDFCFB) and
   --color-surface-alt (#F6F3F1).

   Measured WCAG contrast (canvas / surface-alt):

   | Token                    | Before      | After         |
   |--------------------------|-------------|---------------|
   | --color-text-secondary   | 7.28 / 6.75 | 13.30 / 12.34 |
   | --color-text-faint       | 5.38 / 4.99 |  8.28 /  7.68 |
   | --color-decorative-glyph | 3.43 / 3.18 |  8.28 /  7.68 |
   | --color-border           | 1.24 / 1.15 |  4.76 /  4.42 |
   | --color-border-light     | 1.24 / 1.15 |  4.19 /  3.89 |
   | --color-border-strong    | 1.61 / 1.49 |  8.28 /  7.68 |
   | --color-border-control   | 8.28 / 7.68 |  8.28 /  7.68 |

   --color-text-primary is #110F0E (18.66:1) and needs no override. */
@media (prefers-contrast: more) {
  :root {
    /* Same warm-ink hue, at near-black lightness. */
    --color-text-secondary: #312D2B;
    --color-text-faint: #504C4A;
    /* Still decorative, but no longer a ghost at this setting. */
    --color-decorative-glyph: #504C4A;

    --color-border: #75706F;
    --color-border-light: #7E7977;
    --color-border-strong: #504C4A;
    --color-border-control: #504C4A;
  }
}

/* ---------- Reset ---------- */
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* Keep fragment navigation below the fixed bar, with a small optical gap.
     main.js uses scrollIntoView so scripted and native anchor jumps share this
     one offset policy instead of maintaining a second pixel calculation. */
  scroll-padding-top: calc(var(--nav-height) + var(--space-md));
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font-family);
  /* Relative, never px: an absolute size here would override the browser's
     default font-size setting — the standard low-vision remedy — while every
     `rem` elsewhere in this sheet still scaled with it, inverting the type
     hierarchy. 1rem renders at exactly 16px on an untouched browser. */
  font-size: var(--type-body);
  /* 400, not 500: the app's type scale uses w400/w600/w700 only and its body
     role is w400 (lib/app/utils/constants/styles.dart `_body`), and the body
     font is one of the things this sheet shares with the app rather than
     diverges from. 500 was carried over unannotated from the first commit.
     HIG body copy is Regular. */
  font-weight: 400;
  line-height: 1.5;
  color: var(--color-text-primary);
  background-color: var(--color-canvas);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

/* WebP stills wrap an <img> in <picture>. `display: contents` avoids the
   inline-box baseline gap a default `display: inline` picture adds under the
   image. Do not rely on this for grids of pictures: each <source> becomes a
   grid item too, which is what stacked the sharing-flow phones — see
   `.sharing__visual picture` and `.audience__visual picture`. */
picture {
  display: contents;
}

/* Links are underlined by default and use --color-primary-dark, not
   --color-primary: brand text on light surfaces needs the darker teal.
   Colour alone must never be the only cue that distinguishes a link from body
   copy (WCAG 1.4.1).
   Chrome links whose container already signals interactivity — nav, mobile
   menu, footer nav, store badges and buttons — opt out below. */
a {
  color: var(--color-primary-dark);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  transition: color 0.2s;
}

/* The tint is for PROSE links only. The chrome anchors listed in the
   text-decoration opt-out below carry their own colour identity, and a blanket
   `a:hover/a:active` colour (0,1,1) outranks every single-class rule among them
   (0,1,0) — on `.btn--primary` it repainted the label --color-primary-darker on
   a --color-primary-darker background, i.e. an invisible label. `:where()` keeps
   the negation at the original specificity, so nothing else shifts.
   Touch never fires :hover, so :active carries the same tint for a finger and
   stays outside @media (hover: hover) on purpose. */
a:not(:where(
  .navbar__logo,
  .navbar__links a,
  .navbar__login,
  .lang-switcher__option,
  .mobile-menu a,
  .footer__link-group a,
  .footer__lang,
  .footer__store a,
  .hero__store-group a,
  .cta__badges a,
  .chooser__card,
  .btn
)):hover,
a:not(:where(
  .navbar__logo,
  .navbar__links a,
  .navbar__login,
  .lang-switcher__option,
  .mobile-menu a,
  .footer__link-group a,
  .footer__lang,
  .footer__store a,
  .hero__store-group a,
  .cta__badges a,
  .chooser__card,
  .btn
)):active {
  color: var(--color-primary-darker);
}

.navbar__logo,
.navbar__links a,
.navbar__login,
.lang-switcher__option,
.mobile-menu a,
.footer__link-group a,
.footer__lang,
.footer__store a,
.hero__store-group a,
.cta__badges a,
.chooser__card,
.btn {
  text-decoration: none;
}

ul, ol {
  list-style: none;
}

button {
  font-family: inherit;
  cursor: pointer;
  border: none;
  background: none;
}

input, textarea, select {
  font-family: inherit;
  font-size: inherit;
}

/* ---------- Focus Styles (Accessibility) ---------- */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  /* Brand focus ring: --color-primary-dark on light surfaces. */
  outline: 2px solid var(--color-primary-dark);
  outline-offset: 2px;
}

/* ---------- Skip link ---------- */
/* First focusable element on every page. Off-screen until focused, so it costs
   sighted users nothing but saves keyboard users the navbar plus the seven-item
   language menu on every navigation. */
.skip-link {
  position: absolute;
  top: var(--space-sm);
  left: var(--space-sm);
  z-index: 2000;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-text-on-primary);
  background: var(--color-primary-dark);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-sm) - 4px));
  transition: transform 0.2s;
}

.skip-link:focus {
  transform: translateY(0);
}

/* The skip link parks focus on <main tabindex="-1"> so the next Tab continues
   from the content. It is a container, not a control, and the global
   [tabindex]:focus-visible rule would otherwise ring the whole page.
   Deliberately scoped to <main>: the in-page anchor targets (#features,
   #patients) park focus on their <h2 tabindex="-1">, and that ring is kept —
   it is a small, precise indicator showing a sighted keyboard user where the
   jump landed, and :focus-visible means a mouse click on the same anchor draws
   nothing. Do not widen this to section[tabindex="-1"] or [tabindex="-1"]. */
main[tabindex="-1"]:focus,
main[tabindex="-1"]:focus-visible {
  outline: none;
}

/* ---------- Typography ---------- */
h1, h2, h3, h4, h5 {
  line-height: 1.3;
  font-weight: 600;
}

/* Tracking and leading are tier-conditional, not flat: at phone widths h1/h2 are
   30px/24px — text sizes, not display sizes — and display tracking/leading there
   reads as crowded. The tight values move to the >= 48rem tier below, where the
   type is actually display sized. */
h1, h2 {
  font-family: var(--font-display);
  letter-spacing: -0.015em;
  line-height: 1.15;
  color: var(--color-ink);
}

h1, h2 { font-size: var(--type-display); font-weight: 700; }
h3, h4, h5 { font-size: var(--type-body); }

/* The type scale stays fixed across breakpoints. Responsive layout and spacing,
   rather than additional font sizes, give larger screens their extra room. */
@media (width >= 48rem) {
  h1, h2 {
    letter-spacing: -0.025em;
    line-height: 1.08;
  }
}

/* ---------- Navbar ---------- */
/* The navigation layer is the one place translucency belongs (HIG: Materials):
   content scrolls beneath a blurred bar, which picks up a hairline separator
   once there is content behind it. Do not extend this to content cards. */
.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  /* viewport-fit=cover extends the page under the sensor housing in landscape,
     so the bar's own horizontal padding absorbs the safe-area insets. Padding
     (not margin) keeps the translucent background full-bleed to the edges. */
  padding-left: calc(var(--gutter) + env(safe-area-inset-left));
  padding-right: calc(var(--gutter) + env(safe-area-inset-right));
  background-color: rgba(var(--color-surface-rgb), 0.72);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  /* Hairline is transparent until the page is scrolled (.navbar.is-scrolled). */
  border-bottom: 0.5px solid transparent;
  transition: border-bottom-color 0.3s, background-color 0.3s;
}

.navbar.is-scrolled {
  border-bottom-color: var(--color-border-light);
}

/* Fallback: no backdrop-filter support -> opaque bar. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .navbar {
    background-color: var(--color-surface);
  }
}

/* Web mapping of iOS "Reduce Transparency" -> opaque bar. */
@media (prefers-reduced-transparency: reduce) {
  .navbar {
    background-color: var(--color-surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

.navbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: var(--max-width);
  width: 100%;
  margin: 0 auto;
}

/* Hit-target floor (HIG: 44x44pt). Link-shaped chrome controls carry no padding
   of their own, so their hit region is only as tall as their line box even when
   they sit in a generous container. min-height + centring grows the hit region
   without moving anything: the navbar is a fixed-height centred flex row, so a
   44px child cannot change the bar or its neighbours. */
.navbar__logo {
  display: flex;
  align-items: center;
  min-height: 44px;
  gap: var(--space-sm);
}

/* logo-wordmark.png is a pre-coloured teal asset (#0F5C56, the pre-redesign
   --color-primary-dark), kept as-is by the 2026-09 redesign rather than
   regenerated — it reads as brand mark, not UI chrome, and the hue is close
   enough to the new accent that a mismatch is not visible. No CSS filter —
   see the note on .footer__brand img. */
/* Mobile-first: 26px is the small-phone size. At 320px the bar carries the
   wordmark, the CTA and a 44px hamburger in 288px of usable width, and the CTA
   is the reason it exists, so the wordmark is what gives way. Restored to 40px
   at 30rem. */
.navbar__logo img {
  height: 26px;
  width: auto;
}

/* Gap applies when the bar first shows the links at lg. Audience labels are
   longer than Funktionen/Features; md cannot fit them next to the CTA.
   --space-xl (not --space-xxl) leaves French slack against the language
   switcher at 1024px. */
.navbar__links {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}

.navbar__links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-text-secondary);
  transition: color 0.2s;
  position: relative;
}

/* Nav links opt out of the global underline, so this tint is their only hover
   cue — it is kept, but retinted to --color-primary-dark. */
.navbar__links a:hover,
.navbar__links a:active {
  color: var(--color-primary-dark);
}

/* Nav currently opts out of the global underline, so the current page needs
   its own cue. Primary text plus a 2px brand underline (not colour alone).
   The mobile sheet uses the same marker: those links are already primary
   text, so colour alone would not distinguish the current item. */
.navbar__links a[aria-current="page"],
.mobile-menu a[aria-current="page"] {
  color: var(--color-text-primary);
  text-decoration: underline;
  text-decoration-color: var(--color-primary-dark);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.15em;
}

/* Login sits in navbar__right, next to the language switcher — not in
   .navbar__links, which tops out at four items. Same type as those links.
   Hidden below lg; the mobile sheet carries a copy below lg. nowrap so Iniciar
   sesión cannot become a two-line control if the bar is still tight. */
.navbar__login {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-text-secondary);
  transition: color 0.2s;
  white-space: nowrap;
}

.navbar__login:hover,
.navbar__login:active {
  color: var(--color-primary-dark);
}

/* Small-phone tier; restored to --space-lg at 30rem together with the wordmark
   and the .btn--sm padding. */
.navbar__right {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* Dutch needs the full CTA label "App downloaden"; at 320px it touched the
   wordmark even after the small-phone logo concession. Keep the 44px menu
   target and brand mark intact, and let the hero carry conversion on narrow
   phones. The CTA returns as soon as 360px provides a clean gap. */
.navbar__right > .btn {
  display: none;
}

@media (width >= 22.5rem) {
  .navbar__right > .btn {
    display: inline-flex;
  }
}

/* Two of the three small-phone concessions; the third is .btn--sm's padding and
   type, reverted at the same 30rem breakpoint down in the button block. */
@media (width >= 30rem) {
  .navbar__logo img {
    height: 40px;
  }

  .navbar__right {
    gap: var(--space-lg);
  }
}

/* Language Switcher */
.lang-switcher {
  position: relative;
}

.lang-switcher__button {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  /* Unlike the link-shaped controls this one has a visible border, so raising it
     to the 44px floor does grow its chrome (35px -> 44px). The bar's fixed
     height absorbs it — nothing around it moves — and a bordered control that is
     smaller than its own hit region would be worse. */
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border-control);
  /* Pill and elevation, so the navbar reads as one row of controls: the CTA
     beside it is a --radius-full .btn, and a --radius-sm box next to a pill was
     the one square corner left in the bar. The hairline stays — unlike the
     photo panels, this is a control, and its border is the control boundary
     (WCAG 1.4.11), same reasoning as .btn--outline, which is also bordered
     *and* raised. */
  border-radius: var(--radius-full);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
  transition: border-color 0.2s, color 0.2s, box-shadow 0.15s, transform 0.1s;
  cursor: pointer;
}

.lang-switcher__button:hover,
.lang-switcher__button:active {
  border-color: var(--color-primary-dark);
  color: var(--color-primary-dark);
}

/* Press-in, matching .btn:active. */
.lang-switcher__button:active {
  transform: scale(0.98);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

.lang-switcher__arrow {
  font-size: var(--type-meta);
  transition: transform 0.2s;
}

.lang-switcher.open .lang-switcher__arrow {
  transform: rotate(180deg);
}

.lang-switcher__dropdown {
  position: absolute;
  /* --space-sm, not --space-xs: the first option must sit at least 8px clear of
     the trigger's own (now 44px) hit region. */
  top: calc(100% + var(--space-sm));
  right: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  min-width: 140px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
  z-index: 1001;
}

.lang-switcher.open .lang-switcher__dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* Menu rows are contiguous by construction, so the 8px-between-targets rule does
   not apply here; each row instead carries the full 44px height itself. */
.lang-switcher__flag {
  flex-shrink: 0;
  line-height: 1;
}

.lang-switcher__option {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
  min-height: 44px;
  padding: var(--space-sm) var(--space-lg);
  font-size: var(--type-meta);
  font-weight: 500;
  color: var(--color-text-secondary);
  text-align: left;
  transition: background-color 0.15s, color 0.15s;
  cursor: pointer;
}

.lang-switcher__option:hover,
.lang-switcher__option:active {
  background: var(--color-primary-surface);
  color: var(--color-primary-dark);
}

.lang-switcher__option.active {
  color: var(--color-primary-dark);
  font-weight: 600;
}

/* Mobile Menu — present by default (mobile-first); the inline nav links take
   over from --bp-lg up. The language switcher appears at md; Login at lg. */
.mobile-menu {
  position: fixed;
  top: var(--nav-height);
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--color-surface);
  padding: var(--space-xxl);
  padding-bottom: calc(var(--space-xxl) + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
  /* At 400% zoom a 1280px desktop viewport is only 320 CSS px wide and
     typically short enough that the links plus seven language chips cannot fit.
     The body is deliberately locked while this sheet is open, so the sheet
     itself must own the vertical scroll instead of clipping its last rows. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  transform: translateX(100%);
  transition: transform 0.3s ease-in-out;
  z-index: 999;
}

.mobile-menu.open {
  transform: translateX(0);
}

.mobile-menu a {
  font-size: var(--type-body);
  font-weight: 600;
  color: var(--color-text-primary);
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--color-border-light);
  transition: color 0.15s;
}

/* This menu only exists on touch, so :hover would never fire here — the press
   tint is its only feedback. */
.mobile-menu a:active {
  color: var(--color-primary-dark);
}

.mobile-menu__langs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding-top: var(--space-lg);
}

/* The pre-rendered locale pages render these chips as <a href> links rather than
   buttons, so both element types carry the same chip styling. The anchor rules
   follow `.mobile-menu a` in source order and win at equal specificity. */
.mobile-menu__langs button,
.mobile-menu__langs a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 44px;
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-full);
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  transition: all 0.2s;
}

.mobile-menu__langs button:active,
.mobile-menu__langs a:active {
  border-color: var(--color-primary-dark);
  color: var(--color-primary-dark);
  background: var(--color-primary-surface);
}

/* `.active` is the selected-language state and must beat the transient press
   state above, so it stays last. */
.mobile-menu__langs button.active,
.mobile-menu__langs a.active {
  background: var(--color-primary-dark);
  color: var(--color-text-on-primary);
  border-color: var(--color-primary-dark);
}

.navbar__links,
.navbar__right .lang-switcher,
.navbar__right .navbar__login {
  display: none;
}

.mobile-toggle {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* The bars stay 24x2 with a 5px rhythm; only the button's box reaches 44x44. */
  min-width: 44px;
  min-height: 44px;
  gap: 5px;
  padding: var(--space-sm);
  cursor: pointer;
}

.mobile-toggle span {
  display: block;
  width: 24px;
  height: 2px;
  background: var(--color-text-primary);
  border-radius: 2px;
  transition: transform 0.3s, opacity 0.3s;
}

.mobile-toggle.active span:nth-child(1) {
  transform: rotate(45deg) translate(5px, 5px);
}

.mobile-toggle.active span:nth-child(2) {
  opacity: 0;
}

.mobile-toggle.active span:nth-child(3) {
  transform: rotate(-45deg) translate(5px, -5px);
}

@media (width >= 48rem) {
  .navbar__right .lang-switcher {
    display: flex;
  }
}

@media (width >= 64rem) {
  .mobile-toggle,
  .mobile-menu {
    display: none;
  }

  .navbar__links {
    display: flex;
    gap: var(--space-xl);
  }

  /* Login waits for lg: French labels plus the CTA already spend md's slack,
     and the CTA stays (it is the conversion control). The sheet covers < lg. */
  .navbar__right .navbar__login {
    display: inline-flex;
  }

  /* Held at 0.9375rem through tablet: French labels plus the CTA overflow the
     bar there. lg has more slack (see the gap comment above). */
  .navbar__links a,
  .navbar__login {
    font-size: var(--type-body);
  }
}

/* ---------- Hero Section ---------- */
.hero {
  position: relative;
  display: flex;
  align-items: center;
  padding: calc(var(--nav-height) + var(--hero-pad-y)) var(--gutter) var(--hero-pad-y);
  /* Mint wash on the copy, fading to white before the poster so the still's
     white canvas is not tinted. Diagonal used to run the wash through the
     image; these stops keep it on the text side. */
  background: linear-gradient(180deg, #edf8f7 0%, #f8fbfa 38%, #ffffff 72%);
  overflow: hidden;
}

.hero__inner {
  max-width: var(--max-width);
  width: 100%;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  text-align: center;
  gap: var(--space-xl);
  align-items: center;
}

.hero__content {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  position: relative;
  z-index: 1;
}

/* A quiet status pill, not a filled button: white on the canvas with a
   hairline and a live-dot. The filled version competed with the CTA row. */
.hero__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  background: var(--color-surface);
  color: var(--color-primary-dark);
  padding: 6px var(--space-md) 6px var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  font-size: var(--type-meta);
  font-weight: 600;
  width: fit-content;
  margin: 0 auto;
  letter-spacing: 0.01em;
}

.hero__badge::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.18);
}

/* Leading and tracking are tier-conditional (see the h1/h2 rules). At 320-420pt
   the headline is 32-36px and wraps to three or four lines in German and French;
   1.06 leading there crowds the `span` highlight bar against the line above it.
   The display values move to the >= 64rem tier, where the title reaches
   3.25rem (--type-hero). */
.hero__title {
  font-size: var(--type-hero);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--color-ink);
  max-width: 14ch;
  margin: 0 auto;
}

/* The highlight covers ONE keyword, never a clause. A clause-length span breaks
   across two or three lines and renders as several ragged bars of different
   lengths — and the wrap point is uncontrollable across seven locales, whose
   headline lengths differ by ~40%. One word cannot wrap, so the bar is a single
   deliberate mark at every viewport in every language. `nowrap` enforces that
   even if a future translation puts a space inside the span.
   Keep sentence-final punctuation OUTSIDE the span; it sits mid-sentence, so
   there is no trailing mark left floating past the end of the bar. */
/* The one orange mark in the hero: a marker stroke under the keyword. The
   keyword itself stays ink so the mark is colour *behind* text, never text
   colour (WCAG 1.4.3 does not apply to a decorative band). */
.hero__title span {
  white-space: nowrap;
  color: var(--color-ink);
  background: linear-gradient(180deg, transparent 66%, rgba(245, 166, 35, 0.55) 66%, rgba(245, 166, 35, 0.55) 92%, transparent 92%);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  padding: 0 0.04em;
}

.hero__subtitle {
  font-size: var(--type-body);
  color: var(--color-text-secondary);
  max-width: 36em;
  margin: 0 auto;
  line-height: 1.65;
}

.hero__cta {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  /* Each store group carries a two-line rating line, so the two groups never
     fit side by side below md — the gap is a vertical seam between two stacked
     badges, not a column separator. --space-xxl is restored at md. */
  gap: var(--space-lg);
  flex-wrap: wrap;
}

/* --space-md, not --space-sm: this column is the badge's vertical clear space —
   QR above it, rating line below it. Both stores require a quarter of the badge
   height clear on every side, which is 12px at the 48px badge height. 8px was
   under the floor at 40px too and is further under it now. */
.hero__store-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
}

/* Hit-target floor for the store badge anchors, which shrink-wrap their image.
   At the current 48px badge height the image already clears 44px and this rule
   is a no-op, but it stays as the floor: it is what keeps the anchor tappable if
   the badge height is ever reduced towards Apple's 40px minimum. */
.hero__store-group > a,
.cta__badges a,
.footer__store a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* The <picture> wrapper must not become a flex item of its own, otherwise it
   would still contribute a gap once the QR image is hidden on mobile. */
.hero__qr-frame {
  display: contents;
}

/* display: contents also promotes <source> into the flex container, where it
   becomes a zero-size flex item that still consumes a gap. UA stylesheets do
   not hide <source>, so do it here. */
.hero__qr-frame > source {
  display: none;
}

/* Hidden below --bp-lg: a QR code is pointless on the device you would scan it
   with, and two store badges plus a QR do not fit the hero's content
   column until it is wide enough (~470px, i.e. lg). Showing it at md wraps the
   CTA into a 374px-tall stack — measured at 768 and 834. */
/* The scaled-down version of the media panel above: the codes ship with their
   own white quiet zone, so a --radius-md corner plus --shadow-md turns that
   into a deliberate little card instead of a white square sitting on a
   near-white wash. --radius-md, not --radius-2xl: the radius scale is keyed to
   the size of the surface, and a 32px corner on a 112px tile eats the quiet
   zone the scanner needs. */
.hero__qr {
  display: none;
  width: var(--hero-qr-size);
  height: var(--hero-qr-size);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

/* One store-badge size for the whole page. Both Apple and Google specify their
   badges by HEIGHT with a fixed aspect ratio, and the two artworks have
   *different* ratios (App Store 119.66x40, Google Play 155x46), so sizing by
   width would render them at two different heights side by side. 48px, above
   Apple's 40px web minimum and Google's 28px. The width/height attributes in the
   markup carry each badge's own intrinsic ratio so the reserved box matches and
   no CLS occurs.
   No border-radius here: both artworks ship their own corner geometry, and
   overriding it crops the supplied badge — a prohibited alteration under both
   programmes, and visually a double corner. */
.hero__store-badge,
.cta__store-badge,
.footer__store-badge {
  height: 48px;
  width: auto;
  /* Same elevation as the QR tiles beside them, via `filter`, not
     `box-shadow`: the badge artwork ships its own rounded corners, so a
     rectangular box-shadow would show its own square corners behind them.
     drop-shadow follows the artwork's alpha instead, and adds nothing to the
     badge itself — no crop, no recolour, nothing either programme forbids.
     Blur is roughly half the --shadow-md value, which is how the two read at
     the same weight (drop-shadow's blur is a standard deviation). */
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, 0.12));
  transition: transform 0.2s, opacity 0.2s;
}

@media (hover: hover) {
  .hero__store-badge:hover,
  .cta__store-badge:hover,
  .footer__store-badge:hover {
    transform: scale(1.05);
    opacity: 0.9;
  }
}

/* Press-in, the inverse of the hover lift above. Outside @media (hover: hover):
   this is the only feedback a finger gets. */
.hero__store-badge:active,
.cta__store-badge:active,
.footer__store-badge:active {
  transform: scale(0.96);
  opacity: 0.85;
}

/* Store Rating */
.store-rating {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--type-meta);
  font-weight: 600;
}

/* Decorative by design: the glyphs carry aria-hidden and the adjacent
   .store-rating__text states the same rating in words, so they are not the only
   channel for the information and WCAG 1.4.11's 3:1 does not apply. Amber at
   2.03:1 on white is deliberate — darkening it enough to clear 3:1 pushes it to
   brown and away from the near-universal rating-star convention. Do not re-flag
   in a contrast audit without first removing the text alternative. */
.store-rating__stars {
  color: var(--color-rating);
  letter-spacing: 1px;
}

.store-rating__text {
  color: var(--color-text-secondary);
}

.store-rating--on-dark .store-rating__text {
  color: rgba(var(--color-text-on-primary-rgb), 0.75);
}

/* Shown instead of the App Store badge on locales whose storefront does not
   carry the app yet (see STORE_AVAILABILITY in build-locales.py). */
.store-soon {
  max-width: 32em;
  margin-block: 0;
  margin-inline: auto;
  font-size: var(--type-meta);
  line-height: 1.5;
  text-align: center;
  color: var(--color-text-secondary);
}

/* The hero column turns left-aligned at md, so the note has to follow the badge
   it replaces. The CTA section stays centred at every width. */
@media (width >= 48rem) {
  .hero .store-soon {
    margin-inline: 0;
    text-align: start;
  }
}

.store-soon--on-dark {
  color: rgba(var(--color-text-on-primary-rgb), 0.75);
}

.hero__visual {
  display: flex;
  justify-content: center;
  align-items: center;
}

.hero__video-wrapper {
  position: relative;
  max-width: var(--hero-media-size);
  width: 100%;
  aspect-ratio: 3 / 4;
  /* The still is a bitmap whose canvas is a lavender sweep, not white, so its
     edges cut a rectangle out of the hero's mint-to-white wash. Framing it is
     the fix that survives: the same --radius-2xl panel the editorial stills
     use (.audience__visual), so the canvas reads as a deliberate media surface
     instead of a seam. --shadow-md is scoped to overlaying elements, hero
     media included — see the token's own comment.
     Do not replace this with a feather or a matched backdrop. Both were tried:
     the still's content runs to within 2% of the right edge and 4% of the
     bottom, so a mask eats the feature pills, and a matched plate just moves
     the rectangle a few pixels out. */
  border-radius: var(--radius-2xl);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  background: transparent;
}

/* The global `picture { display: contents }` would dump this picture's
   <source> siblings into the wrapper box; keep a real picture box so the img
   can fill it. */
.hero__video-wrapper picture,
.hero__video {
  border-radius: inherit;
}

.hero__video-wrapper picture {
  display: block;
  width: 100%;
  height: 100%;
  position: relative;
  z-index: 1;
}

.hero__video {
  width: 100%;
  height: 100%;
  object-fit: contain;
}


.hero__video-spinner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-surface-variant);
  transition: opacity 0.4s ease-out;
}

.hero__video-spinner.hidden {
  opacity: 0;
  pointer-events: none;
}

/* Manual play affordance — shown when autoplay is deliberately skipped
   (data-saver, slow connection, reduced motion) and after playback ends. */
.hero__video-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 64px;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: rgba(var(--color-primary-dark-rgb), 0.85);
  color: var(--color-text-on-primary);
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(var(--color-shadow-rgb), 0.25);
  transition: background 0.2s ease, transform 0.2s ease;
}

.hero__video-play svg {
  width: 28px;
  height: 28px;
  margin-left: 3px; /* optical centering of the triangle */
}

@media (hover: hover) and (pointer: fine) {
  .hero__video-play:hover {
    background: rgba(var(--color-primary-dark-rgb), 1);
    transform: translate(-50%, -50%) scale(1.05);
  }
}

/* Press-in; the translate half of the transform is the centring offset and must
   be repeated or the button jumps to the corner. */
.hero__video-play:active {
  background: rgba(var(--color-primary-dark-rgb), 1);
  transform: translate(-50%, -50%) scale(0.94);
}

.hero__video-play.is-hidden {
  display: none;
}

@media (width >= 48rem) {
  .hero {
    background: linear-gradient(90deg, #edf8f7 0%, #f8fbfa 28%, #ffffff 50%);
  }

  .hero__inner {
    grid-template-columns: 1fr 1fr;
    text-align: start;
    gap: var(--space-xl);
    align-items: center;
  }

  .hero__visual {
    position: relative;
    min-width: 0;
    min-height: 0;
  }

  .hero__title {
    margin: 0;
  }

  .hero__badge {
    margin: 0;
  }

  /* Drop the centred-hero measure cap: the subtitle now fills the content
     column so it holds weight against the video, rather than sitting as a
     480px island in a ~600px track. */
  .hero__subtitle {
    margin: 0;
    max-width: none;
    font-size: var(--type-body);
  }

  .hero__content {
    gap: var(--space-xl);
  }

  .hero__cta {
    gap: var(--space-xl);
  }

}

@media (width >= 64rem) {
  .hero__inner {
    gap: var(--space-xxl);
  }

  /* 1.06, not 1.04: from this tier the headline is >= 52px and the last line
     is often the highlighted keyword. At 1.04 the marker band clears the
     descenders of the line above by a hair in German and touches them in
     French ("finissent dans un / tiroir"). */
  .hero__title {
    line-height: 1.06;
    letter-spacing: -0.03em;
  }

  /* One step over body copy, and a measure of its own. A 16px subtitle under a
     52px headline reads as a caption, and left unconstrained it runs the full
     ~750px column while the headline above it stops at 14ch — two different
     ragged edges in the same block. */
  .hero__subtitle {
    font-size: 1.125rem;
    max-width: 30ch;
  }

  .hero__qr {
    display: block;
  }

  /* The two store columns must share horizontal lines — QRs on one, badges
     on one. Nested flex columns cannot do that: each column aligns only
     against its own box, so the ~550px one-line App Store rating stretched
     column 1 and left its QR and badge floating in the middle of it. One grid
     on the parent, with both groups neutralised to `display: contents`, gives
     the columns genuinely shared rows. Rows: 1 QR, 2 badge, 3 the Android
     note, 4 the App Store rating spanning both tracks. */
  .hero__cta,
  .cta__badges {
    display: grid;
    grid-template-columns: max-content max-content;
    justify-items: start;
    align-items: center;
    /* 24px between the two badges and 12px above/below the badge row — the
       latter is the store-mandated clear space at a 48px badge. Do not reduce. */
    column-gap: var(--space-xl);
    row-gap: var(--space-md);
  }

  .hero__cta {
    justify-content: start; /* neutralises the base `center` */
  }

  .cta__badges {
    display: inline-grid;
    justify-content: center;
    justify-items: center;
  }

  .hero__store-group {
    display: contents;
  }

  /* Explicit modifiers, not :nth-child: build-locales.py deletes the whole App
     Store group on locales without a storefront, so a positional selector would
     assign Google Play to the empty first column there. */
  .hero__store-group--apple > * {
    grid-column: 1;
  }

  .hero__store-group--play > * {
    grid-column: var(--hero-play-col, 2);
  }

  .hero__cta:not(:has(.hero__store-group--apple)),
  .cta__badges:not(:has(.hero__store-group--apple)) {
    grid-template-columns: max-content;
    --hero-play-col: 1;
  }

  .hero__qr {
    grid-row: 1;
  }

  .hero__store-group > a {
    grid-row: 2;
  }

  .hero__store-group--play .store-rating {
    grid-row: 3;
  }

  /* Full-width bottom row, sized so it wraps instead of dictating track width.
     A grid item spanning both tracks still contributes its max-content width to
     them, and a `ch` cap alone is not enough: in the system font stack 1ch is
     ~9.3px, so 40ch measures 373px — 44px more than the 329px badge+gap sum, and
     it grew both tracks by 22px each (measured at 1024/1280/1440). `width: 0`
     zeroes the contribution; the percentage min-width resolves against the grid
     area afterwards, so the caption still fills the block and wraps to two lines
     without ever feeding back into track sizing.
     Do not add a `max-width` here expecting it to cap the measure: min-width is
     resolved last and always beats max-width, so alongside `min-width: 100%` any
     max-width is inert (verified — a 100px cap against a 600px area still used
     600px). The measure is bounded by the track sum instead, which is the two
     badge widths plus the column gap (~329px, ~35ch). If the block ever grows
     wide enough to need a real reading-measure cap, put it on the inner
     `.store-rating__text`, which is not a grid item and so cannot feed back into
     track sizing.
     The wording itself is BGH I ZR 143/23 (value + count + as-of date) and is
     built from placeholders — only its box changes here. */
  .hero__store-group--apple .store-rating {
    grid-row: 4;
    grid-column: 1 / -1;
    align-items: flex-start;
    width: 0;
    min-width: 100%;
  }
}

@media (width >= 80rem) {
  /* xxl, not xxxl: the headline now grows into the copy column, so the extra
     12px between the columns reads as a hole rather than as breathing room. */
  .hero__inner {
    gap: var(--space-xxl);
  }
}

@media (width >= 100rem) {
  .hero__inner {
    grid-template-columns: 1.05fr 1fr;
  }
}

/* Hero staggered entrance */
@keyframes heroRise {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: translateY(0); }
}

.hero__content > * {
  animation: heroRise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.hero__content > *:nth-child(1) { animation-delay: 0.05s; }
.hero__content > *:nth-child(2) { animation-delay: 0.13s; }
.hero__content > *:nth-child(3) { animation-delay: 0.21s; }
.hero__content > *:nth-child(4) { animation-delay: 0.29s; }
.hero__content > *:nth-child(5) { animation-delay: 0.37s; }
.hero__visual {
  animation: heroRise 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.22s both;
}

/* ---------- Trust Strip ---------- */
/* A hairline row on the canvas, not a tinted band: the hero above already
   carries the mint, and the strip should read as a caption to it. */
.trust-strip {
  padding: 0 var(--gutter);
  position: relative;
  z-index: 1;
  background: var(--color-canvas);
}

.trust-strip__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-lg) 0;
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Row gap is deliberately tighter than the column gap: at four short chips the
     strip is two rows on a small phone and one row from the md tier up, so the
     row axis needs far less air than the column axis. */
  gap: var(--space-sm) var(--space-xl);
  flex-wrap: wrap;
}

.trust-strip__item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--color-text-secondary);
  font-size: var(--type-meta);
  font-weight: 600;
  white-space: nowrap;
}

/* Inline SVG in the same text run as the label. Sized in em so it tracks the
   label at every tier. The glyph carries the brand tint; the label stays
   secondary so the row reads as a caption, not as four buttons. */
.trust-strip__icon {
  display: inline-flex;
  flex: none;
  color: var(--color-primary-dark);
}

.trust-strip__icon svg {
  width: 1.15em;
  height: 1.15em;
}

@media (width >= 48rem) {
  .trust-strip__inner {
    justify-content: space-between;
    padding: var(--space-xl) 0;
    gap: var(--space-xxl);
  }

  .trust-strip__item {
    font-size: var(--type-meta);
  }
}

/* ---------- Audience chooser ---------- */
/* Two whole-card links directly under the fold. This is the page's fork:
   a therapist and a patient arrive with different questions, and a chooser
   answers "which half of this page is for me" before either scrolls. */
.chooser {
  padding: var(--section-y-md) var(--gutter) 0;
}

.chooser__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
}

.chooser__card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-xl);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  color: var(--color-text-primary);
  text-decoration: none;
  overflow: hidden;
  transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
}

/* The patient card is the tinted one: it is the download path, and the
   tint tells a patient with a code in their hand "this one". */
.chooser__card--patient {
  background: var(--color-primary-surface);
  border-color: var(--color-primary-light);
}

@media (hover: hover) and (pointer: fine) {
  .chooser__card:hover {
    border-color: var(--color-primary);
    transform: translateY(-2px);
    box-shadow: 0 12px 32px rgba(var(--color-ink-rgb), 0.08);
  }

  .chooser__card:hover .chooser__arrow {
    transform: translateX(4px);
  }
}

.chooser__card:active {
  transform: translateY(0);
  border-color: var(--color-primary-dark);
}

.chooser__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--type-meta);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-primary-dark);
}

.chooser__title {
  font-family: var(--font-display);
  font-size: var(--type-body);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--color-ink);
}

.chooser__desc {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.55;
}

/* The card is the link; this row is its visible affordance. It is a span,
   not a nested button, so there is exactly one tab stop per card. */
.chooser__cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  min-height: 44px;
  font-weight: 700;
  color: var(--color-primary-dark);
}

.chooser__arrow {
  width: 20px;
  height: 20px;
  flex: none;
  transition: transform 0.2s;
}

@media (width >= 48rem) {
  .chooser__inner {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-xl);
  }

  .chooser__card {
    padding: var(--space-xxl);
    gap: var(--space-md);
  }

  .chooser__title {
    font-size: var(--type-body);
  }

  .chooser__desc {
    font-size: var(--type-body);
  }
}

@media (width >= 80rem) {
  .chooser__title {
    font-size: var(--type-display);
  }
}

/* ---------- Section Common ---------- */
.section {
  padding: var(--section-y-md) var(--gutter);
}

.section--alt {
  background: var(--color-surface-alt);
  padding: var(--section-y-lg) var(--gutter);
}

/* The one dark chapter (#sharing). Mid brand teal, not near-black ink.
   Every text and hairline colour inside is taken from the on-ink ramp;
   nothing here reads --color-text-secondary. */
/* #sharing's own larger --section-y-lg rhythm. Previously a dark ink-filled
   "chapter" band (.section--ink) — the redesign has no full-bleed dark
   surface anywhere, so that class was renamed away rather than kept as a
   misleading name for a plain section. */
#sharing {
  padding: var(--section-y-lg) var(--gutter);
}

.section__inner {
  max-width: var(--max-width);
  margin: 0 auto;
}

.section__header {
  text-align: center;
  margin-bottom: var(--space-xxl);
}

.section__header h2 {
  margin-bottom: var(--space-md);
}

.section__header p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.6;
  max-width: 40rem;
  margin: 0 auto;
}

/* Left-aligned headers for the chapter sections. The measure cap stays; the
   text just no longer floats in the middle of a 1200px band. */
.section__header--start {
  text-align: start;
}

.section__header--start p {
  margin: 0;
  max-width: 36rem;
}

.section__header--start h2 {
  max-width: 22ch;
}

@media (width >= 48rem) {
  .section__header {
    margin-bottom: var(--space-xxxl);
  }

  .section__header p {
    font-size: var(--type-body);
    max-width: 44rem;
  }

  .section__header--start p {
    max-width: 38rem;
  }
}

/* Eyebrow: a short label above every chapter heading, in the brand teal. Used
   to carry a numbered index ("01" … "05") too; the redesign dropped it (it
   read as decoration nobody needed to navigate by), so this is label-only. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
  font-size: var(--type-meta);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-primary-dark);
}

/* ---------- Features Section (therapist + patient audience splits) ---------- */
.audience {
  max-width: var(--max-width);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--feature-row-gap);
  /* Centered, not start-aligned: the photo and the copy column are rarely the
     same height, and top-aligning left a photo that reads as floating lower
     than the heading next to it. */
  align-items: center;
}

.audience__copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
  min-width: 0;
}

.audience__header {
  text-align: start;
}

.audience__header h2 {
  margin-bottom: var(--space-md);
}

.audience__header p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.6;
  margin: 0;
}

/* Photo first on a phone. Desktop order is copy | photo (therapist);
   .audience--photo-start flips it so the patient block mirrors it. */
/* Elevated media panel, the same treatment as .hero__video-wrapper: every
   photographic still on the landing page is a --radius-2xl panel lifted off
   the canvas with --shadow-md. The shadow is what keeps a still whose own
   background is not the page's background (the clay hero, the desk laptop's
   warm room) from ending on a seam. */
.audience__visual {
  order: -1;
  min-width: 0;
  overflow: hidden;
  border-radius: var(--radius-2xl);
  box-shadow: var(--shadow-md);
}

/* Two phones in the scene, so the picture must stay `display: block` — the
   global `picture { display: contents }` would dump the sources into the
   audience grid (the same failure that stacked the sharing phones). */
.audience__visual picture,
.sharing__visual picture {
  display: block;
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius-2xl);
}

.audience__image,
.sharing__image {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-2xl);
}

/* Editorial stills are 1080x1440 (3:4). Shorten the frame by capping the
   column, not by clipping the bitmap. Desk keeps the uncapped laptop still. */
.audience:not(.audience--desk) .audience__visual {
  max-width: min(100%, calc(var(--feature-image-max) * 3 / 4));
  justify-self: center;
}

.audience:not(.audience--desk) .audience__image {
  width: 100%;
  height: auto;
}

/* Feature list: hairline rows inside a light frame so the group lifts off
   the cream canvas. Not a stack of separate cards. */
.feature-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--color-surface);
}

.feature-list__item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-lg);
  padding: var(--space-lg);
  border-bottom: 1px solid var(--color-border-light);
}

.feature-list__item:last-child {
  border-bottom: none;
}

/* Icon tiles carry inline SVG, never emoji: emoji are rendered by the OS font
   (Apple glossy / Noto flat / Segoe), so the same page looked like three
   different designs, and several glyphs (the EU flag) do not render at all on
   Windows. The SVG set is one stroke language shared across the page and
   inherits colour via currentColor. */
.feature-list__icon {
  width: 44px;
  height: 44px;
  flex: none;
  background: var(--color-primary-surface);
  border: 1px solid var(--color-primary-light);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-primary-dark);
}

.feature-list__icon svg {
  width: 22px;
  height: 22px;
}

.feature-list__body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.feature-list__body h3 {
  color: var(--color-ink);
  font-size: var(--type-body);
  line-height: 1.3;
  margin: 0;
}

.feature-list__body p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.55;
  margin: 0;
}

.audience__cta {
  margin-top: var(--space-sm);
}

@media (width >= 48rem) {
  /* The stills are 3:2 and size their own frame; the copy column centres
     beside them rather than stretching the photo to the copy's height. */
  .audience {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-xxl);
    align-items: center;
  }

  .audience__visual {
    order: 0;
  }

  .audience--photo-start .audience__visual {
    order: -1;
  }

  .audience__header p {
    font-size: var(--type-body);
  }

  .feature-list__item {
    padding: var(--space-xl);
  }

  .feature-list__body h3 {
    font-size: var(--type-body);
  }

  .feature-list__body p {
    font-size: var(--type-body);
  }
}

@media (width >= 64rem) {
  .audience {
    gap: var(--space-xxxl);
  }

  .audience__copy {
    padding-block: 0;
  }
}

@media (width >= 80rem) {
  .audience {
    gap: 64px;
  }

  .feature-list__body h3 {
    font-size: var(--type-body);
  }

  .feature-list__body p {
    font-size: var(--type-body);
  }
}

/* ---------- Testimonial ---------- */
/* A bordered, tinted card — not text floating on the bare canvas with only a
   coloured rule to anchor it. Matches the other card surfaces (.bento__cell,
   .pro-card) rather than the pre-redesign quote-with-a-stripe treatment. */
.testimonial {
  position: relative;
  max-width: 720px;
  margin: 0 auto;
  padding: var(--space-xl);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
}

.testimonial__stars {
  display: flex;
  gap: 3px;
  margin-bottom: var(--space-md);
  color: var(--color-rating);
}

.testimonial__stars svg {
  width: 16px;
  height: 16px;
}

.testimonial__quote {
  margin: 0;
}

.testimonial__quote p {
  font-family: var(--font-display);
  font-size: var(--type-body);
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: -0.015em;
  color: var(--color-ink);
}

.testimonial__author {
  margin-top: var(--space-lg);
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.testimonial__info {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--space-sm);
  row-gap: 2px;
  text-align: left;
}

.testimonial__name {
  font-weight: 700;
  font-size: var(--type-meta);
  color: var(--color-text-primary);
}

.testimonial__role {
  color: var(--color-text-secondary);
  font-size: var(--type-meta);
}

/* Name and role on one line, separated by a middle dot. */
.testimonial__role::before {
  content: '\00B7';
  margin-right: var(--space-sm);
  color: var(--color-border-control);
}

/* The source label is a link to the storefront it came from, so a sceptical
   reader can go and read the review themselves — that verifiability is the whole
   point of naming the store. Muted like the surrounding meta, but it keeps the
   global underline: colour alone must not be the only cue (WCAG 1.4.1).
   Where the app is not published in a locale's storefront, build-locales.py
   unwraps this back into a span. */
.testimonial__source {
  display: inline-flex;
  align-items: center;
  flex-basis: 100%;
  gap: 4px;
  font-size: var(--type-meta);
  color: var(--color-text-secondary);
  /* 44 px tap target; the extra height is absorbed by matching negative margins
     so the author block keeps its rhythm. */
  min-height: 44px;
  margin: -11px 0 -13px;
}

.testimonial__store-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  opacity: 0.7;
}

/* The patient voice closes the patient feature list itself: it is evidence for
   the workflow above, not a disconnected chapter that follows it. */
.feature-list__item--testimonial {
  display: block;
  padding: var(--space-xl);
  background: var(--color-surface-alt);
}

.feature-list__item--testimonial .testimonial {
  max-width: none;
  margin: 0;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
}

/* The therapist review closes the three-step sharing list, following the
   same embedded-proof pattern as the patient review below its feature list. */
.sharing-steps__item--testimonial {
  display: block;
  padding: var(--space-xl);
  background: var(--color-surface-alt);
}

.sharing-steps__item--testimonial .testimonial {
  max-width: none;
  margin: 0;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
}

@media (width >= 48rem) {
  .testimonial {
    padding: var(--space-xl) 0 var(--space-xl) var(--space-xxl);
  }

  .feature-list__item--testimonial .testimonial {
    padding: 0;
  }

  .sharing-steps__item--testimonial .testimonial {
    padding: 0;
  }

  .testimonial__quote p {
    font-size: var(--type-body);
  }
}

@media (width >= 80rem) {
  .testimonial {
    max-width: 800px;
  }

  .testimonial__quote p {
    font-size: var(--type-body);
  }
}

/* ---------- Everything included (bento) ---------- */
/* Six capability cells of two sizes on the mint band. Cells are <li> so a
   screen reader hears "list, 6 items"; nothing in a cell is clickable, so
   there is no hover state (a lift on a plain div promises a click that does
   not exist). */
.bento {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}

.bento__cell {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-xl);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  overflow: hidden;
}

.bento__cell h3 {
  color: var(--color-ink);
  font-size: var(--type-body);
  line-height: 1.3;
  margin: 0;
}

.bento__cell p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.55;
  margin: 0;
}

.bento__icon {
  width: 44px;
  height: 44px;
  flex: none;
  margin-bottom: var(--space-sm);
  background: var(--color-primary-surface);
  border: 1px solid var(--color-primary-light);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-primary-dark);
}

.bento__icon svg {
  width: 22px;
  height: 22px;
}

/* --ink keeps the wide grid span. Fill and type match the sibling cells so
   the EU claim is not a dark slab on the mint band. The lock icon inherits
   the shared teal tile. */
.bento__cell--ink {
  background: var(--color-surface);
  border-color: var(--color-border);
}

@media (width >= 48rem) {
  .bento {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-lg);
  }

  .bento__cell {
    padding: var(--space-xxl);
    gap: var(--space-sm);
  }

  .bento__cell--wide,
  .bento__cell--ink {
    grid-column: span 2;
  }

  .bento__cell h3 {
    font-size: var(--type-body);
  }

  .bento__cell p {
    font-size: var(--type-body);
  }
}

@media (width >= 64rem) {
  .bento {
    grid-template-columns: repeat(4, 1fr);
  }

  .bento__cell--wide {
    grid-column: span 2;
  }

  .bento__cell--ink {
    grid-column: span 2;
  }
}

/* ---------- Physiopass Pro ---------- */
.pro-section {
  padding-top: var(--section-y-lg);
  padding-bottom: var(--section-y-lg);
}

.pro-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  max-width: none;
  margin: 0;
}

/* Two plans, two surfaces: Gratis is a white card with a hairline; Pro is
   the same card with an ink header block. The header carries name, price
   and badge; the body is the check list. No orange border — the accent is
   the badge text, once. */
.pro-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
}

.pro-card__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-xl);
  border-bottom: 1px solid var(--color-border);
}

/* Free, not Pro, gets the teal-bordered, teal-tinted highlight treatment
   (matching the source design's accent-bordered pricing panel) — most people
   using Physiopass are on the free tier, and that's the card this product
   wants to look like the obvious, comfortable choice, not the one it's
   quietly upselling away from. Pro stays a plain bordered card, same shape,
   no separate dark header block; every child stays on the normal
   ink/secondary text ramp. */
.pro-card--free {
  background: var(--color-primary-surface);
  border-width: 1.5px;
  border-color: var(--color-primary);
}

.pro-card--free .pro-card__head {
  border-bottom-color: var(--color-primary-light);
}

.pro-card h3 {
  color: var(--color-ink);
  margin: 0;
  font-family: var(--font-display);
  /* The plan name carries the free-card header on its own. Giving both names
     the display tier makes that space feel intentional instead of like a
     missing price row. */
  font-size: var(--type-display);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
}

.pro-card__badge {
  align-self: flex-start;
  font-family: var(--font-mono);
  font-size: var(--type-meta);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-primary-dark);
}

.pro-card__badge:empty {
  display: none;
}

.pro-card__price {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-top: var(--space-xs);
}

.pro-card__price-amount {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--type-display);
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--color-text-primary);
}

.pro-card__price-unit {
  font-size: var(--type-body);
  color: var(--color-text-secondary);
}

.pro-card__price-note {
  margin: 0;
  font-size: var(--type-meta);
  color: var(--color-text-secondary);
}

.pro-card__price-note:empty {
  display: none;
}

.pro-card__list {
  list-style: none;
  margin: 0;
  padding: var(--space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  flex: 1 1 auto;
}

.pro-card__list li {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  align-items: start;
  column-gap: var(--space-sm);
  color: var(--color-text-primary);
  font-size: var(--type-body);
  line-height: 1.5;
}

.pro-card__feature-icon {
  display: grid;
  place-items: center;
  width: 20px;
  height: 24px;
  color: var(--color-primary-dark);
}

.pro-card__feature-icon svg {
  width: 20px;
  height: 20px;
  margin-top: 2px;
}

.pro-card__footnote {
  margin: 0;
  padding: 0 var(--space-xl) var(--space-xl);
  font-size: var(--type-meta);
  color: var(--color-text-secondary);
}

/* Under the plans: the practice-billing sentence, then the CTA row. The note
   is deliberately not a third card — it is one fact that applies to both. */
.pro-cta {
  margin-top: var(--space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border);
}

.pro-cta__note {
  margin: 0;
  font-size: var(--type-body);
  font-weight: 600;
  color: var(--color-ink);
  max-width: 36rem;
}

.pro-cta__actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-md);
}

.pro-cta__hint {
  margin: 0;
  font-size: var(--type-meta);
  color: var(--color-text-secondary);
  text-align: center;
}

@media (width >= 48rem) {
  .pro-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-xl);
  }

  .pro-card__head,
  .pro-card__list {
    padding: var(--space-xxl);
  }

  .pro-card__footnote {
    padding: 0 var(--space-xxl) var(--space-xxl);
  }

  .pro-cta {
    margin-top: var(--space-xxl);
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-xl);
  }

  .pro-cta__actions {
    flex-direction: row;
    align-items: center;
    gap: var(--space-lg);
    flex: none;
  }

  .pro-cta__hint {
    text-align: start;
    max-width: 18rem;
  }
}

/* ---------- Sharing Flow (ink band) ---------- */
/* Header on top, then two columns: numbered steps left, the still right.
   The still is framed in a lighter ink so it does not disappear into the
   band, and on desktop it drops below the band's bottom edge. */
.sharing {
  max-width: var(--max-width);
  margin: 0 auto;
}

.sharing__header {
  text-align: start;
  margin-bottom: var(--space-xxl);
}

.sharing__header h2 {
  margin-bottom: var(--space-md);
  max-width: 20ch;
}

.sharing__header p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.6;
  margin: 0;
}

.sharing__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xxl);
  align-items: start;
}

/* Same elevated panel as .audience__visual. The hairline border and tinted
   fill this used to carry are gone: a border *and* a shadow read as two frames
   around one photo, and the hairline was there to give the panel an edge
   before the stills were elevated. */
.sharing__visual {
  min-width: 0;
  overflow: hidden;
  border-radius: var(--radius-2xl);
  box-shadow: var(--shadow-md);
}

/* Same bordered-card treatment as .feature-list — a bare hairline-divided
   list here (this section's previous, pre-redesign shape) read as a lower
   level of polish than every other feature list on the page. */
.sharing-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--color-surface);
}

.sharing-steps__item {
  display: flex;
  flex: 1;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-lg) var(--space-xl);
  border-bottom: 1px solid var(--color-border);
  text-align: start;
}

.sharing-steps__item:last-child {
  border-bottom: 0;
}

/* Mono index in the accent, like the section eyebrows — one numbering
   language for the whole page. */
.sharing-steps__num {
  flex: none;
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--type-meta);
  letter-spacing: 0.04em;
  /* --color-primary-dark (brand teal), not --color-accent (amber): this is a
     list marker, not a rating — amber stays isolated to star ratings so the
     page only shows one "accent" moment at a time. */
  color: var(--color-primary-dark);
  min-width: 2.5ch;
}

.sharing-steps__body {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.55;
}

/* Each step is one short sentence: use weight to lead into it, rather than
   adding a separate title scale between the section header and its body copy. */
.sharing-steps__body h3,
.sharing-steps__body p {
  display: inline;
  font-size: inherit;
  line-height: inherit;
  margin: 0;
}

.sharing-steps__body h3 {
  color: var(--color-text-primary);
}

.sharing-steps__separator {
  display: inline;
}

@media (width >= 48rem) {
  .sharing__header p {
    font-size: var(--type-body);
  }

  .sharing__grid {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-xxxl);
    align-items: center;
  }

  .sharing-steps__item {
    padding: var(--space-xl) var(--space-xxl);
    align-items: center;
  }

  .sharing-steps__body {
    font-size: var(--type-body);
  }
}

.section__header--spaced {
  margin-top: var(--section-y-md);
}

/* ---------- Contact Section ---------- */
.contact {
  padding: var(--section-y-md) var(--gutter);
}

.contact__inner {
  max-width: 640px;
  margin: 0 auto;
}

.contact__header {
  text-align: center;
  margin-bottom: var(--space-xl);
}

/* h1, not h2: this is the page's own title and the page had no level-one
   heading at all. Sizes pinned to the h2 scale so only the semantics change. */
.contact__header h1 {
  color: var(--color-primary-dark);
  margin-bottom: var(--space-md);
  font-size: var(--type-body);
  font-weight: 700;
}

@media (width >= 48rem) {
  .contact__header h1 { font-size: var(--type-display); }

  .contact__header p {
    font-size: var(--type-body);
  }

  .contact__header {
    margin-bottom: var(--space-xxl);
  }
}

.contact__header p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.65;
}

.contact__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.form-group label {
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-text-primary);
}

.form-group input,
.form-group textarea {
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-primary);
  transition: border-color 0.2s;
}

.form-group input:focus,
.form-group textarea:focus {
  /* No outline: none here — the global :focus-visible ring must survive. */
  border-color: var(--color-primary-dark);
  box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);
}

.form-group textarea {
  resize: vertical;
  min-height: 120px;
}

/* Inline validation message. Empty by default and left in the DOM rather than
   [hidden]: aria-describedby on the input points at it permanently, and a
   display:none target resolves to no accessible description at all.
   It is also never display:none while *empty*, which would be the same mistake
   one step removed: a role="alert" that is out of the accessibility tree at the
   moment its text is written is a live region being *added* with content
   already in it, which several screen readers do not announce. Kept rendered,
   an empty <p> is zero-height (no margins), so this costs one flex gap and
   nothing else — and the input's accessible description still resolves to none
   while the text is empty. */
.form-error {
  font-size: var(--type-meta);
  color: var(--color-error);
}

/* Keep the permanent live-region nodes in the accessibility tree without
   charging every untouched field a second flex gap. Once validation inserts
   text, :empty stops matching and the message gets its normal separation. */
.form-error:empty {
  margin-top: calc(-1 * var(--space-sm));
}

.form-group input[aria-invalid="true"],
.form-group textarea[aria-invalid="true"] {
  border-color: var(--color-error);
}

.form-group label.form-consent {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  min-height: 44px;
  font-weight: 400;
  cursor: pointer;
}

.form-consent input[type="checkbox"] {
  width: 22px;
  height: 22px;
  margin-top: 0.15em;
  flex-shrink: 0;
  accent-color: var(--color-primary-dark);
}

.contact__success {
  text-align: center;
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-success);
  margin-top: var(--space-sm);
}

.contact__error {
  text-align: center;
  font-size: var(--type-meta);
  font-weight: 600;
  color: var(--color-error);
  margin-top: var(--space-sm);
}

.contact__email-note {
  text-align: center;
  font-size: var(--type-meta);
  color: var(--color-text-secondary);
  margin-top: var(--space-sm);
}

.contact__email-note a {
  font-weight: 600;
}

.contact-page {
  padding: calc(var(--nav-height) + var(--space-xxl)) var(--gutter) var(--section-y-md);
  min-height: 100vh; /* fallback */
  min-height: 100svh;
}

/* ---------- Error Page (404) ---------- */
.error-page {
  padding: calc(var(--nav-height) + var(--space-xxl)) var(--gutter) var(--section-y-md);
  min-height: 100vh; /* fallback */
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.error-page__inner {
  max-width: 560px;
}

.error-page__code {
  color: var(--color-primary-dark);
  font-size: var(--type-display);
  font-weight: 700;
  line-height: 1;
  margin-bottom: var(--space-md);
}

.error-page__inner h1 {
  color: var(--color-primary-dark);
  font-size: var(--type-display);
  margin-bottom: var(--space-sm);
}

.error-page__message {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  margin-bottom: var(--space-xl);
}

.error-page__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  justify-content: center;
}

/* ---------- CTA Section ---------- */
.cta {
  padding: var(--section-y-md) var(--gutter) var(--section-y-lg);
  text-align: center;
}

/* Plain, panel-less: the redesign's final CTA is centred text directly on the
   page canvas, not a dark panel — matching every other section, so the
   footer under it is not a return to a different surface. */
.cta__inner {
  max-width: var(--max-width);
  margin: 0 auto;
}

.cta__inner h2 {
  color: var(--color-text-primary);
  font-size: var(--type-display);
  margin-bottom: var(--space-md);
}

.cta__subtitle {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.6;
  margin-bottom: var(--space-xxl);
}

.cta__badges {
  display: inline-grid;
  grid-template-columns: minmax(0, 18rem);
  justify-content: center;
  gap: var(--space-md);
  max-width: 100%;
}

/* Plain, like the hero's own store groups — no card wrapper. A bordered card
   here read as a second, competing visual pattern for the same "download the
   app" action the hero already presents unboxed. */
.cta__badges .hero__store-group {
  display: flex;
  min-width: 0;
}

.cta__badges .hero__store-group > a {
  width: 100%;
  justify-content: center;
}

/* Generator places the App Store coming-soon line after the store cards, on
   the ink, so the --on-dark tint stays. Unused while every locale has an
   App Store URL; keep the hook so a future None does not inherit card styling. */
.cta__inner > .store-soon {
  margin-top: var(--space-lg);
}

@media (width >= 48rem) {
  .cta__inner h2 {
    font-size: var(--type-display);
  }

  .cta__subtitle {
    font-size: var(--type-body);
  }

  .cta__badges {
    grid-template-columns: repeat(2, minmax(0, 13rem));
    gap: var(--space-lg);
  }
}

@media (width >= 64rem) {
  .cta__badges {
    grid-template-columns: max-content max-content;
    column-gap: var(--space-xxl);
    row-gap: var(--space-lg);
  }

  /* The final action is the conversion moment: make its QR codes and store
     badges materially larger than their supporting counterparts in the hero. */
  .cta__badges .hero__qr {
    width: 192px;
    height: 192px;
    /* 192px is a card, not a chip — one radius step up from the hero's 112px
       tile, same as the rest of the scale's size keying. */
    border-radius: var(--radius-lg);
  }

  .cta__store-badge {
    height: 64px;
  }
}

@media (width >= 80rem) {
  .cta__inner {
    padding: calc(var(--space-xxxl) + var(--space-md)) var(--space-xxxl);
  }

  .cta__inner h2 {
    font-size: var(--type-display);
  }
}

/* ---------- Buttons ---------- */
/* One pill language for compact chrome: --radius-full and font-weight 700
   match .hero__badge. The 1.5px transparent border makes the filled and
   outline variants geometrically identical so they never differ by a border
   width when placed side by side (as they are on 404.html).
   line-height/text-align matter once a long localized label wraps at 320px. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-xl);
  font-size: var(--type-body);
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  border-radius: var(--radius-full);
  /* Same elevation as the page's media panels, QR tiles and store badges: a
     control is a thing that sits ON the page, so it carries --shadow-md like
     everything else that does. Both variants get it, so the filled and
     outline pills still read as one family. */
  box-shadow: var(--shadow-md);
  transition: background-color 0.2s, border-color 0.2s, color 0.2s,
    box-shadow 0.15s, transform 0.1s;
  min-height: 48px;
  cursor: pointer;
  border: 1.5px solid transparent;
  -webkit-tap-highlight-color: transparent;
}

/* Resting background is --color-ink (the redesign's primary-button fill);
   hover/active swap to the brand accent, not a darker black — the one place
   the teal shows up on an otherwise monochrome control, matching the source
   design's own hover treatment. */
.btn--primary {
  background: var(--color-ink);
  color: var(--color-text-on-primary);
  border-color: var(--color-ink);
}

/* --color-ink, not the brand accent: the label is button text and the
   border is a control boundary (WCAG 1.4.11), so both stay on the neutral
   ink/line ramp at rest. */
.btn--outline {
  background: transparent;
  color: var(--color-ink);
  border-color: var(--color-border);
}

/* Navbar and mobile chrome only — never in page content. 44px is the WCAG 2.5.8
   floor and matches the hamburger and the language trigger beside it, so the bar
   reads as one row of controls. Mobile-first, like the rest of the sheet: this
   is the small-phone tier, widened at 30rem in the navbar block above. The hit
   target is never part of that — only the padding and the type. */
.btn--sm {
  min-height: 44px;
  padding: var(--space-sm);
  font-size: var(--type-meta);
}

/* Off the small-phone tier, together with the wordmark and the .navbar__right
   gap in the navbar block. */
@media (width >= 30rem) {
  .btn--sm {
    padding-inline: var(--space-lg);
    font-size: var(--type-meta);
  }
}

.btn--full {
  width: 100%;
}

/* Pressed state. Deliberately OUTSIDE @media (hover: hover): a finger press must
   produce the same colour change a mouse hover does — the transform alone is too
   subtle on a phone. prefers-reduced-motion zeroes the transition globally, after
   which the scale applies instantly, which is correct for a state. */
.btn:active {
  transform: scale(0.98);
  /* Press-in: the pill drops toward the page instead of only shrinking. */
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

.btn--primary:active {
  background: var(--color-primary-dark);
  border-color: var(--color-primary-dark);
}

.btn--outline:active {
  background: var(--color-surface-alt);
  border-color: var(--color-ink);
}

/* Hover is a desktop enhancement and carries no information: every state a touch
   or keyboard user needs exists without it (see :active above and the global
   focus ring). */
@media (hover: hover) {
  .btn--primary:hover {
    background: var(--color-primary-dark);
    border-color: var(--color-primary-dark);
  }

  .btn--outline:hover {
    background: var(--color-surface-alt);
    border-color: var(--color-ink);
    color: var(--color-ink);
  }
}

/* An explicit fill, not opacity: opacity on a filled brand pill lands somewhere
   unpredictable against whatever sits behind it, while this pair is a fixed
   ~5.9:1. contact.html's submit hits this while sending. */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--color-border-light);
  border-color: var(--color-border-light);
  color: var(--color-text-secondary);
  cursor: default;
  transform: none;
  /* A disabled control is not raised — nothing about it is actionable. */
  box-shadow: none;
  pointer-events: none;
}

/* ---------- Footer ---------- */
.footer {
  color: var(--color-text-primary);
  padding: var(--space-xxxl) var(--gutter) var(--space-xl);
  border-top: 1px solid var(--color-border);
}

.footer__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
  text-align: start;
  gap: var(--space-xl);
}

.footer__brand {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-md);
}

.footer__langs {
  grid-column: 1 / -1;
}

/* Navbar and footer share one asset, assets/img/logo-wordmark.png: the
   trademark wordmark with every opaque pixel set to the brand foreground
   --color-primary-dark (#0F5C56) and the antialiasing alpha preserved. It
   replaces a hue-rotate/brightness filter over the teal source
   (logo_text.png), which produced an approximation of the brand rather than
   the token itself and blew out the lighter pixels over the hero gradient.
   Do not bake the app's purple AppColors into this asset.

   Regenerate from logo_text.png with:
     python3 -c "from PIL import Image; s=Image.open('logo_text.png').convert('RGBA'); \
       h=128; im=s.resize((round(s.width*h/s.height),h), Image.LANCZOS); a=im.getchannel('A'); \
       Image.merge('RGBA',[Image.new('L',im.size,c) for c in (0x0F,0x5C,0x56)]+[a]) \
       .save('logo-wordmark.png', optimize=True)" */
.footer__brand > img {
  height: 28px;
  width: auto;
  max-width: 180px;
  object-fit: contain;
}

.footer__brand p {
  font-size: var(--type-meta);
  color: var(--color-text-secondary);
  max-width: 280px;
}

.footer__store {
  display: grid;
  grid-template-columns: 64px max-content;
  align-items: center;
  column-gap: var(--space-md);
  row-gap: var(--space-sm);
}

/* A footer is a secondary conversion point, so its badges stay compact while
   their anchors retain the shared 44px touch floor. QR codes sit beside their
   corresponding badge rather than forcing readers to hunt for a scan target. */
.footer__store-badge {
  height: 36px;
}

/* Same elevated tile as .hero__qr, one radius step down for a 64px surface.
   The hairline is gone: a border plus a shadow reads as two frames, and the
   codes already carry their own white quiet zone. */
.footer__qr {
  display: block;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}

.footer__link-group {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs);
  min-width: 0;
  text-align: start;
}

.footer__link-group h2 {
  font-size: var(--type-meta);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-primary-dark);
}

/* Footer nav links are standalone navigation, not prose, so they take the 44px
   floor. This is the one place it changes desktop layout: the footer's link
   columns grow (nothing above them moves). The 12px column gap already clears
   the 8px separation floor, so it is left alone. */
.footer__link-group a:not(.footer__lang) {
  display: flex;
  align-items: center;
  min-height: 44px;
  min-width: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  transition: color 0.2s;
}

/* Footer nav links opt out of the global underline, so as in the navbar the
   tint is the hover cue: --color-primary-dark. */
.footer__link-group a:not(.footer__lang):hover,
.footer__link-group a:not(.footer__lang):active {
  color: var(--color-primary-dark);
}

.footer__lang-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.footer__lang {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--type-meta);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-text-secondary);
  background: var(--color-surface);
  border: 1px solid var(--color-border-control);
  transition: background-color 0.15s, color 0.15s, border-color 0.15s;
}

.footer__lang:hover,
.footer__lang:active {
  border-color: var(--color-primary-dark);
  color: var(--color-primary-dark);
}

.footer__lang.active {
  background: var(--color-primary-dark);
  color: var(--color-text-on-primary);
  border-color: var(--color-primary-dark);
}

.footer__lang.active:hover,
.footer__lang.active:active {
  background: var(--color-primary-dark);
  color: var(--color-text-on-primary);
  border-color: var(--color-primary-dark);
}

.footer__bottom {
  max-width: var(--max-width);
  margin: var(--space-xxl) auto 0;
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border-light);
  text-align: start;
  font-size: var(--type-meta);
  color: var(--color-text-secondary);
}

@media (width >= 48rem) {
  .footer {
    padding: var(--section-y-lg) var(--gutter) var(--space-xxl);
  }

  .footer__inner {
    grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
    gap: var(--space-xxl);
  }

  .footer__brand,
  .footer__langs {
    grid-column: auto;
  }

  .footer__bottom {
    margin-top: var(--space-xxxl);
    padding-top: var(--space-xl);
  }
}

/* ---------- Secondary pages: content cards ---------- */
/* Contact form, release months and legal text sit in a white card with a
   hairline on the canvas, so these pages share the landing page's surfaces. */
.contact__form,
.releases-month,
.legal-page__content {
  background: var(--color-surface);
  border-radius: var(--radius-xl);
  border: 1px solid var(--color-border);
  padding: var(--space-xl);
}

@media (width >= 48rem) {
  .contact__form,
  .releases-month,
  .legal-page__content {
    padding: var(--space-xxl);
  }
}

/* ---------- Legal Pages ---------- */
.legal-page {
  padding: calc(var(--nav-height) + var(--space-xxl)) var(--gutter) var(--section-y-md);
  min-height: 100vh; /* fallback */
  min-height: 100svh;
}

.legal-page__inner {
  /* A character-based measure stays readable across system fonts and browser
     default-size changes; the former 46rem rendered 736px wide (~85–90
     characters) on desktop. */
  max-width: 70ch;
  margin: 0 auto;
}

.legal-page__content {
  line-height: 1.7;
  color: var(--color-text-primary);
}

.legal-page__content h1 {
  color: var(--color-primary-dark);
  margin-bottom: var(--space-lg);
  font-size: var(--type-body);
  overflow-wrap: anywhere;
}

.legal-page__content h2 {
  color: var(--color-primary-dark);
  margin-top: var(--space-xl);
  margin-bottom: var(--space-md);
  font-size: var(--type-body);
  overflow-wrap: anywhere;
}

.legal-page__content h3 {
  color: var(--color-text-primary);
  margin-top: var(--space-xl);
  margin-bottom: var(--space-md);
  font-size: var(--type-body);
  overflow-wrap: anywhere;
}

.legal-page__content p {
  margin-bottom: var(--space-md);
}

.legal-page__content ul,
.legal-page__content ol {
  margin-bottom: var(--space-md);
  padding-left: var(--space-xl);
}

.legal-page__content ul {
  list-style: disc;
}

.legal-page__content ol {
  list-style: decimal;
}

.legal-page__content li {
  margin-bottom: var(--space-xs);
}

/* Legal copy cites authority URLs in full (e.g. the BfDI address page), which
   are single unbreakable tokens far wider than a phone viewport and scroll the
   whole page sideways. Break them anywhere rather than clip or overflow. */
.legal-page__content a {
  overflow-wrap: anywhere;
}

@media (width >= 30rem) {
  .legal-page__content h1 {
    font-size: var(--type-body);
  }
}

@media (width >= 48rem) {
  .legal-page {
    padding-top: calc(var(--nav-height) + var(--space-xxxl));
  }

  .legal-page__content h1 {
    margin-bottom: var(--space-xl);
    font-size: var(--type-display);
  }

  .legal-page__content h2 {
    margin-top: var(--space-xxl);
    margin-bottom: var(--space-lg);
    font-size: var(--type-body);
  }

  .legal-page__content p,
  .legal-page__content ul,
  .legal-page__content ol {
    margin-bottom: var(--space-lg);
  }

  .legal-page__content li {
    margin-bottom: var(--space-sm);
  }
}

/* ---------- Releases Page ---------- */
.releases-page {
  padding: calc(var(--nav-height) + var(--space-xxl)) var(--gutter) var(--section-y-md);
  min-height: 100vh; /* fallback */
  min-height: 100svh;
  background: var(--color-surface-alt);
}

.releases-page__inner {
  max-width: 800px;
  margin: 0 auto;
}

.releases-page__header {
  text-align: center;
  margin-bottom: var(--space-xxl);
}

.releases-page__header h1 {
  color: var(--color-primary-dark);
  font-size: var(--type-display);
  margin-bottom: var(--space-sm);
}

.releases-page__header p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
}

.releases-month {
  margin-bottom: var(--space-xl);
}

.releases-month__title {
  color: var(--color-primary-dark);
  font-size: var(--type-body);
  padding-bottom: var(--space-sm);
  /* Deliberate --color-primary: a purely decorative rule under a heading whose
     text is already --color-primary-dark. It carries no information and is not
     a control boundary, so WCAG 1.4.11 does not apply (2.66:1 on white). */
  border-bottom: 2px solid var(--color-primary);
  margin-bottom: var(--space-lg);
}

.releases-month__entries {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.release-entry {
  background: var(--color-surface);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius-md);
  padding: calc(var(--space-lg) + var(--space-xs));
}

/* No :hover: a .release-entry is a static <article>, not a link. See .why-card. */

.release-entry__date {
  display: inline-block;
  font-size: var(--type-meta);
  font-weight: 700;
  color: var(--color-primary-dark);
  background: var(--color-primary-surface);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-sm);
}

.release-entry__title {
  font-size: var(--type-body);
  color: var(--color-primary-dark);
  margin-bottom: var(--space-sm);
}

.release-entry__desc {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.6;
}

@media (width >= 48rem) {
  .releases-page {
    padding-top: calc(var(--nav-height) + var(--space-xxxl));
  }

  .releases-page__header {
    margin-bottom: var(--space-xxxl);
  }

  .releases-month {
    margin-bottom: var(--space-xxl);
  }

  .releases-month__title {
    margin-bottom: var(--space-xl);
  }

  .release-entry {
    padding: var(--space-xl);
  }

  .release-entry__desc {
    line-height: 1.7;
  }
}

/* ---------- Blog ("In der Praxis") ---------- */
.blog-page {
  padding-top: calc(var(--nav-height) + var(--space-xxl));
  background: var(--color-surface-alt);
}

.blog-page__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--gutter) var(--section-y-md);
}

.blog-hero {
  max-width: 640px;
  margin: 0 auto var(--space-xxl);
  text-align: center;
}

.blog-hero h1 {
  font-family: var(--font-display);
  color: var(--color-primary-dark);
  font-size: var(--type-display);
  margin-bottom: var(--space-md);
}

.blog-hero p {
  color: var(--color-text-secondary);
  font-size: var(--type-body);
  line-height: 1.6;
  margin-bottom: var(--space-lg);
}

.blog-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
}

.blog-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
}

@media (hover: hover) and (pointer: fine) {
  .blog-card:hover {
    border-color: var(--color-primary);
    transform: translateY(-2px);
    box-shadow: 0 12px 32px rgba(var(--color-ink-rgb), 0.08);
  }
}

.blog-card__link {
  display: flex;
  flex-direction: column;
  height: 100%;
  color: inherit;
  text-decoration: none;
}

.blog-card__thumb {
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--color-primary-surface);
}

.blog-card__placeholder,
.blog-detail__hero--placeholder {
  display: grid;
  place-items: center;
  background:
    linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.14), transparent 62%),
    var(--color-surface-alt);
  color: var(--color-primary-dark);
}

.blog-card__placeholder span,
.blog-detail__hero--placeholder span {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  font-size: var(--type-meta);
  font-weight: 600;
}

.blog-card__thumb img,
.blog-detail__hero img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.blog-card__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-lg);
}

.blog-card__title {
  color: var(--color-primary-dark);
  font-size: var(--type-body);
  margin: 0;
}

.blog-card__hook {
  flex: 1;
  color: var(--color-text-secondary);
  font-size: var(--type-meta);
  line-height: 1.5;
  margin: 0;
}

/* ---------- Blog Detail ---------- */
.blog-detail {
  padding: calc(var(--nav-height) + var(--space-xxl)) var(--gutter) 0;
}

.blog-detail__inner {
  max-width: 720px;
  margin: 0 auto;
}

.blog-breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
  color: var(--color-text-secondary);
  font-size: var(--type-meta);
}

.blog-breadcrumb a {
  color: var(--color-primary-dark);
  font-weight: 600;
  text-decoration: none;
}

.blog-breadcrumb a:hover {
  text-decoration: underline;
}

.blog-breadcrumb [aria-current="page"] {
  color: var(--color-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.blog-detail__inner h1 {
  color: var(--color-primary-dark);
  font-size: var(--type-body);
  margin-bottom: var(--space-sm);
}

.blog-detail__hero {
  aspect-ratio: 3 / 2;
  overflow: hidden;
  margin-bottom: var(--space-xl);
  border-radius: var(--radius-lg);
  background: var(--color-primary-surface);
}

.blog-detail__body {
  color: var(--color-text-primary);
  font-size: var(--type-body);
  line-height: 1.7;
  margin-bottom: var(--space-xxl);
}

.blog-detail__body p {
  margin-bottom: var(--space-lg);
}

.blog-related {
  margin-bottom: var(--space-xxl);
}

.blog-related h2 {
  color: var(--color-primary-dark);
  font-size: var(--type-body);
  margin-bottom: var(--space-lg);
}

.blog-related__list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
  list-style: none;
  padding: 0;
  margin: 0;
}

.blog-related__item {
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius-md);
  transition: border-color 0.2s;
}

.blog-related__item:hover {
  border-color: var(--color-primary);
}

.blog-related__item a {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-lg);
  color: inherit;
  text-decoration: none;
}

.blog-related__item h3 {
  color: var(--color-primary-dark);
  font-size: var(--type-body);
  margin: 0;
}

.blog-related__item p {
  color: var(--color-text-secondary);
  font-size: var(--type-meta);
  margin: 0;
}

@media (width >= 48rem) {
  .blog-page {
    padding-top: calc(var(--nav-height) + var(--space-xxxl));
  }

  .blog-hero h1 {
    font-size: var(--type-display);
  }

  .blog-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .blog-detail {
    padding-top: calc(var(--nav-height) + var(--space-xxxl));
  }

  .blog-detail__inner h1 {
    font-size: var(--type-display);
  }

  .blog-related__list {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (width >= 64rem) {
  .blog-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ---------- FAQ Section ---------- */
/* Two columns on desktop: the heading in a sticky left column, the list on
   the right, so a reader scanning eight questions always sees what section
   they are in. Rows are hairline-separated, not boxed. */
.faq-section {
  /* Eight closed questions are naturally dense content; the supporting
     section rhythm keeps this chapter from becoming a full-screen scroll. */
  padding: var(--section-y-sm) var(--gutter);
}

.faq__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
}

.faq__intro h2 {
  margin: 0;
  max-width: 14ch;
}

.faq__list {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--color-border);
}

.faq__item {
  border-bottom: 1px solid var(--color-border);
}

.faq__question {
  padding: var(--space-lg) 0;
  font-size: var(--type-body);
  font-weight: 600;
  color: var(--color-ink);
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  user-select: none;
  -webkit-user-select: none;
  transition: color 0.15s;
}

@media (hover: hover) and (pointer: fine) {
  .faq__question:hover {
    color: var(--color-primary-dark);
  }
}

/* The <summary> is the control, not the <details> wrapper, so the press state
   lives here and works for touch, mouse and the keyboard activation flash. */
.faq__question:active {
  color: var(--color-primary-darker);
}

.faq__question::-webkit-details-marker {
  display: none;
}

.faq__question::marker {
  content: '';
}

/* Plus that rotates into a cross when open — drawn, not a glyph, so it is
   the same in every system font. */
.faq__question::after {
  content: '';
  flex: none;
  width: 28px;
  height: 28px;
  border: 1px solid var(--color-border-control);
  border-radius: 50%;
  background:
    linear-gradient(var(--color-primary-dark), var(--color-primary-dark)) center / 12px 1.5px no-repeat,
    linear-gradient(var(--color-primary-dark), var(--color-primary-dark)) center / 1.5px 12px no-repeat;
  transition: transform 0.25s, border-color 0.15s;
}

.faq__item[open] .faq__question::after {
  transform: rotate(45deg);
  border-color: var(--color-primary-dark);
}

.faq__answer {
  padding: 0 0 var(--space-lg);
}

.faq__answer p {
  color: var(--color-text-secondary);
  line-height: 1.65;
  font-size: var(--type-body);
  max-width: 60ch;
}

@media (width >= 48rem) {
  .faq__question {
    /* 16px top and bottom still yields a 56px target with the text line, well
       above the 44px touch floor while reducing eight rows substantially. */
    padding: var(--space-lg) 0;
    font-size: var(--type-body);
  }

  .faq__answer {
    padding: 0 var(--space-xxxl) var(--space-xl) 0;
  }

  .faq__answer p {
    font-size: var(--type-body);
  }
}

@media (width >= 64rem) {
  .faq__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
    gap: var(--space-xxxl);
    align-items: start;
  }

  .faq__intro {
    position: sticky;
    top: calc(var(--nav-height) + var(--space-xl));
  }
}

/* ---------- Animations ---------- */
.fade-in {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.fade-in.visible {
  opacity: 1;
  transform: translateY(0);
}

.fade-in-left {
  opacity: 0;
  transform: translateX(-30px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.fade-in-left.visible {
  opacity: 1;
  transform: translateX(0);
}

.fade-in-right {
  opacity: 0;
  transform: translateX(30px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.fade-in-right.visible {
  opacity: 1;
  transform: translateX(0);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0ms !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .hero__content > *,
  .hero__visual {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ---------- Utilities ---------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

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

/* ---------- Loading State ---------- */
.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid var(--color-border-light);
  border-top-color: var(--color-primary-dark);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Skeleton for the legal pages, whose body is fetched as Markdown at runtime.
   A centred spinner on a blank page tells the reader nothing about what is
   coming and shifts the whole layout when it lands; a heading/paragraph shape
   reserves it. The shimmer is decoration only — the skeleton is aria-hidden and
   the container carries aria-busy. */
.legal-skeleton {
  padding-top: var(--space-xs);
}

.legal-skeleton__group {
  margin-bottom: var(--space-xxl);
}

.legal-skeleton__line {
  height: 1rem;
  margin-bottom: var(--space-md);
  border-radius: var(--radius-sm);
  background: var(--color-border-light);
  background-image: linear-gradient(
    90deg,
    var(--color-border-light) 0%,
    var(--color-surface-variant) 50%,
    var(--color-border-light) 100%
  );
  background-size: 200% 100%;
  animation: skeletonShimmer 1.6s ease-in-out infinite;
}

.legal-skeleton__line--title {
  height: 2rem;
  width: 65%;
  margin-bottom: var(--space-xl);
}

.legal-skeleton__line--heading {
  height: 1.375rem;
  width: 45%;
  margin-bottom: var(--space-lg);
}

.legal-skeleton__line--short {
  width: 70%;
}

@keyframes skeletonShimmer {
  to { background-position: -200% 0; }
}

/* ---------- Legal load failure ---------- */
.legal-error {
  padding: var(--space-xxl) 0;
}

.legal-error__text {
  margin-bottom: var(--space-lg);
}

/* The blanket reduced-motion block near the top of this section is written for
   one-shot transitions: it makes them resolve instantly, which is correct.
   Applied to an *infinite* animation it does the opposite of what the user
   asked for — a 0.001ms spin repeats forever at ~1000x speed. Both infinite
   animations on this site are therefore cancelled by name and replaced with a
   static indicator. This block must stay *after* the rules it overrides:
   animation-name:none loses to a later `animation:` shorthand regardless of the
   media query, and !important on animation-duration cannot revive a null name. */
@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation-name: none;
    /* Static ring: a partial arc still reads as "busy" without moving. */
    border-color: var(--color-border-light);
    border-top-color: var(--color-primary-dark);
    border-right-color: var(--color-primary-dark);
  }

  .legal-skeleton__line {
    animation-name: none;
    background-image: none;
  }
}

/* ---------- Cookie Consent ---------- */
.cookie-consent {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1100;
  /* Mirrors the app's bottomSheetTheme: an opaque surface with rounded top
     corners, not a square full-bleed bar (which reads as a Material snackbar).
     The radius is the site's --radius-xl, not the app's AppRadius.xl — the two
     scales diverge deliberately (see the Radius block at the top of this file).
     overflow:hidden keeps the inner content clipped inside the corner arc.
     No grabber: the sheet is not swipe-dismissible, and a drag handle that does
     not drag is a false affordance. */
  background: var(--color-surface);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  overflow: hidden;
  box-shadow: 0 -4px 24px rgba(var(--color-shadow-rgb), 0.08);
  animation: consentSlideUp 0.3s ease-out;
}

.cookie-consent__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  /* The inset lives on the inner container, not on .cookie-consent: the banner
     background must still reach the physical bottom edge (no white strip under
     it), while the Accept button has to clear the home indicator. */
  padding: var(--space-lg);
  padding-bottom: calc(var(--space-lg) + env(safe-area-inset-bottom));
  padding-left: calc(var(--space-lg) + env(safe-area-inset-left));
  padding-right: calc(var(--space-lg) + env(safe-area-inset-right));
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: start;
  gap: var(--space-md);
  position: relative;
}

.cookie-consent__close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text-secondary);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  transition: background-color 0.15s ease, color 0.15s ease;
}

.cookie-consent__close:hover {
  background: rgba(var(--color-shadow-rgb), 0.05);
  color: var(--color-text-primary);
}

.cookie-consent__close:active {
  background: rgba(var(--color-shadow-rgb), 0.1);
}

.cookie-consent__close svg {
  width: 20px;
  height: 20px;
}

.cookie-consent__text {
  flex: 1;
  margin: 0;
  padding-right: 44px;
  font-size: var(--type-meta);
  line-height: 1.5;
  color: var(--color-text-secondary);
}

.cookie-consent__text a {
  margin-left: var(--space-xs);
}

.cookie-consent__btn {
  flex-shrink: 0;
  width: 100%;
  white-space: nowrap;
}

/* cookie-consent.js measures from the banner's top edge to the viewport bottom,
   so this reservation follows wrapping localized copy, text zoom, safe areas
   and the floating desktop offset. The extra 16px keeps the last page control
   from sitting flush against the sheet. */
body.has-cookie-consent {
  padding-bottom: calc(var(--cookie-consent-height, 0px) + var(--space-lg));
}

.cookie-consent--closing {
  animation: consentSlideDown 0.25s ease-in forwards;
}

/* 120%, not 100%: on the md tier and up the sheet floats clear of the bottom
   edge, so a 100% translation would leave a sliver of it on screen mid-slide.
   The overshoot is invisible on the full-bleed mobile form. */
@keyframes consentSlideUp {
  from { transform: translateY(120%); }
  to { transform: translateY(0); }
}

@keyframes consentSlideDown {
  from { transform: translateY(0); }
  to { transform: translateY(120%); }
}

@media (width >= 48rem) {
  /* Wide screens: a centred, width-constrained card rather than a bar spanning
     a 1600px viewport — the iOS sheet form factor, all four corners rounded
     because no edge is flush with the screen any more.
     This tier also catches landscape phones (844/852/932pt), where the notch
     insets are horizontal, so left/right carry the safe-area insets rather than
     0. A percentage width would not help: on a position:fixed box percentages
     resolve against the viewport, not against the inset box. Instead width
     stays auto so the box fills left..right, max-width clamps it, and the auto
     side margins centre it *within the safe area*. At this breakpoint the
     viewport is at least 768px against a 704px cap, so there is always a
     >= 32px side gutter without setting one. */
  .cookie-consent {
    left: env(safe-area-inset-left);
    right: env(safe-area-inset-right);
    width: auto;
    max-width: 44rem;
    margin: 0 auto var(--space-lg);
    border-radius: var(--radius-xl);
    bottom: env(safe-area-inset-bottom);
  }

  .cookie-consent__inner {
    flex-direction: row;
    align-items: center;
    text-align: start;
    gap: var(--space-xl);
    /* The card itself now carries the safe-area offset and the horizontal
       margin, so the inner padding is the plain sheet padding. */
    padding: var(--space-lg);
    padding-right: calc(var(--space-lg) + 44px);
  }

  .cookie-consent__text {
    padding-right: 0;
  }

  .cookie-consent__btn {
    width: auto;
  }

  .cookie-consent__close {
    top: var(--space-lg);
    right: var(--space-lg);
  }

}

/* Desktop: photos match the height of the copy column beside them, cropped
   with object-fit rather than floating as a shorter frame. */
@media (width >= 48rem) {
  .audience,
  .sharing__grid {
    align-items: stretch;
  }

  .audience .audience__visual,
  .audience:not(.audience--desk) .audience__visual,
  .sharing__visual {
    max-width: none;
    justify-self: stretch;
    position: relative;
    min-height: 20rem;
  }

  .audience__visual picture,
  .sharing__visual picture {
    position: absolute;
    inset: 0;
    height: 100%;
  }

  .audience .audience__image,
  .audience:not(.audience--desk) .audience__image,
  .sharing__image {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}
