/* =========================================================================
   SlideHome website
   -------------------------------------------------------------------------
   The colour, radius and control tokens below are transcribed from the
   SlideEcho repo (Echo-MVP/src/app/globals.css + tailwind.config.ts, which are
   themselves a transcription of Echo-MVP/DESIGN.md). Do NOT invent a colour
   here. If a new one is needed, add it to Echo's DESIGN.md first, then mirror
   it into this file, so the website and the product cannot drift apart.

   The LAYOUT is the website's own, rebuilt from the Figma frame "Website 8"
   (SLH-Website, node 1:402): one ruled frame with a visible rail down each
   side, horizontal dividers between sections, and every section a text column
   on the left with its evidence on the right.

   SCALE. Figma draws at 1728px wide with a 1507px content column. This file
   runs a 1440px frame, so every size taken from the design is divided by
   roughly 1.2: the 90px headline becomes 75px, the 50px section heading 42px,
   the 20px body 16.5px. Dark is the design and the default; light is derived.
   ========================================================================= */

/* ── Self-hosted fonts (GDPR: no Google Fonts CDN) ──────────────────────
   Variable files, so one request per family covers weights 100 to 900.
   Copied out of the `geist` npm package (v1.7.2), which is the same family
   Echo loads through next/font. Figma draws in Inter and DM Sans; Geist is
   kept because it is what the product wears and the two are close cousins. */
@font-face {
    font-family: 'Geist';
    src: url('../fonts/Geist-Variable.woff2') format('woff2-variations');
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Geist Mono';
    src: url('../fonts/GeistMono-Variable.woff2') format('woff2-variations');
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

/* ── Tokens: light ─────────────────────────────────────────────────────── */
:root {
    /* Brand. A filled primary always carries near-black text: white on the
       violet is 3.0:1, near-black is 5.7:1. */
    --primary: #a69cf8;
    --primary-hover: #9185f6;
    --primary-soft: #ebe9fe;
    --primary-ink: #867be0;
    --primary-strong: #9185f6;
    /* Read once by the twinkle field. Must never be given a dark value. */
    --primary-select: #a99ffa;
    --on-primary: #1a1a1a;

    /* Ink. A very slightly cool ramp, so the greys sit with the violet. */
    --ink-primary: #1a1a1a;
    --ink-secondary: #3e3e44;
    --ink-tertiary: #5a5a62;
    --ink-quaternary: #7a7a85;
    --ink-faint: #9e9ea8;
    --ink-faint-2: #b4b4be;
    --ink-faint-3: #c9c9d1;

    /* Lines. Three tiers, not interchangeable. */
    --border-default: #e3e3e7;
    --border-control: #dedee3;
    --border-subtle: #d2d2d9;
    --divider-row: #f2f2f4;
    --divider-faint: #ececef;

    /* Surfaces */
    --surface-page: #ffffff;
    --surface-sidebar: #efeff0;
    --surface-subtle: #f7f7f8;
    --surface-control-band: #fafafb;
    --surface-active: #e1e1e4;
    --surface-thumbnail: #eeeef0;
    --surface-inverse: #1a1a1a;

    --hover-fill-light: #e7e7e8;
    --surface-selected: #cfcfd4;

    /* Scrims carry their own alpha, never an opacity modifier. */
    --scrim: rgba(20, 20, 26, 0.62);

    --color-success-fg: #2f9e44;
    --color-success-bg: #ebfbee;
    --color-error-fg: #e03131;
    --color-error-bg: #fff5f5;

    --shadow-dropdown: 0 10px 30px rgba(10, 10, 10, 0.12);
    --shadow-compact-popover: 0 16px 40px rgba(10, 10, 10, 0.18), 0 2px 6px rgba(10, 10, 10, 0.08);

    /* ── Website-only geometry ─────────────────────────────────────────── */
    --frame: 1440px;          /* the ruled frame the whole page lives inside */
    /* Figma insets the ruled column 110px from each edge of its 1728 artboard.
       Holding that margin is what makes the side rails visible as rails
       instead of as two lines stuck to the window. */
    --inset: 92px;
    --gutter: 40px;           /* inner padding of the frame, Figma's 48 ÷ 1.2 */
    /* Every chapter is the same shape: one text column, then a panel of this
       width holding its evidence. One number, so the three panels cannot
       drift out of line with each other. */
    --media-col: 52%;
    --story-h: 660px;
    /* The ground the devices stand on. Light has no design of its own here,
       so it gets the quietest surface that is still visibly a panel. */
    --surface-media: #f1f1f3;
    --rail: var(--border-default);
    --nav-h: 70px;            /* Figma 84 ÷ 1.2 */
    --radius-control: 8px;    /* Figma rounds the CTA to 8 */
    --radius-card: 10px;
    --radius-media: 14px;
    --radius-pop: 12px;

    color-scheme: light;
}

/* ── Tokens: dark ──────────────────────────────────────────────────────────
   This is the designed theme, and the default. Only what has to change is
   redeclared. The brand violets and --on-primary are the same in both themes
   on purpose: each is a filled colour carrying near-black on itself, so its
   contrast does not depend on the page underneath. --primary-select in
   particular must stay put, because the twinkle field reads it once, when it
   starts. */
:root[data-theme='dark'] {
    --primary-soft: #353348;
    --primary-ink: #b8b0ff;

    --ink-primary: #f1f1f3;
    --ink-secondary: #c9c9ce;
    --ink-tertiary: #a3a2a9;   /* Figma's body grey, exactly */
    --ink-quaternary: #82828c;
    --ink-faint: #62626b;
    --ink-faint-2: #505058;
    --ink-faint-3: #424248;

    /* Figma rules the frame in #434248. That is the visible grid of the whole
       design, so --rail gets it; the control borders stay quieter. */
    --border-default: #343338;
    --border-control: #3a3940;
    --border-subtle: #434248;
    --divider-row: #212124;
    --divider-faint: #242427;

    --rail: #434248;
    --surface-media: #2a292e;

    --surface-page: #19181c;   /* Figma's page black */
    --surface-sidebar: #121214;
    --surface-subtle: #1e1d22;
    --surface-control-band: #1c1c1f;
    --surface-active: #2b2b2e;
    --surface-thumbnail: #232326;
    --surface-inverse: #343437;

    --hover-fill-light: #27272a;
    --surface-selected: #303033;

    --scrim: rgba(8, 8, 10, 0.7);

    --color-success-fg: #51cf66;
    --color-success-bg: #1e3024;
    --color-error-fg: #ff6b6b;
    --color-error-bg: #392124;

    --shadow-dropdown: 0 10px 30px rgba(0, 0, 0, 0.45);
    --shadow-compact-popover: 0 16px 40px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.3);

    color-scheme: dark;
}

/* ── Language (EN / DE) ───────────────────────────────────────────────────
   Every translatable string that is pure text is written TWICE in the
   markup, once per language, as sibling spans; this pair of rules is the
   entire mechanism that decides which one renders. The language is read
   from <html data-lang>, set before first paint by the same inline script
   that already resolves the theme (see the <head> of any page), so there
   is never a translation flash in either direction — not just on the
   default load, the same as the returning reader whose stored choice is
   English. A handful of strings cannot be two static spans (a sentence
   built from user input, an attribute value, <title>) and are swapped by
   i18n.js instead; everything else is just these two lines. */
html:not([data-lang='en']) .lang-en {
    display: none;
}
html[data-lang='en'] .lang-de {
    display: none;
}

/* ── Reset / base ──────────────────────────────────────────────────────── */
*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    /* The navbar's section links. Turned off again under reduced motion, in
       the block at the end of this file. */
    scroll-behavior: smooth;
    /* `clip`, not `hidden`: `overflow: hidden` here would make the root a
       scroll container and break the navbar's `position: sticky`. */
    overflow-x: clip;
}

body {
    margin: 0;
    background: var(--surface-page);
    color: var(--ink-primary);
    font-family: 'Geist', ui-sans-serif, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
    font-size: 16px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-synthesis-weight: none;
}

/* Numbers, counts and timestamps are always mono (Echo DESIGN.md). */
.num,
.mono {
    font-family: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;
    font-variant-numeric: tabular-nums;
}

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

h1,
h2,
h3,
p,
figure {
    margin: 0;
}

a {
    color: inherit;
}

/* Echo replaces the user agent's blue ring with its own, which never renders
   blue. Same two halves here. */
:focus {
    outline: none;
}
:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: var(--radius-control);
}

::selection {
    background: var(--primary-select);
    color: var(--on-primary);
}

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

/* ═══════════════════════════════════════════════════════════════════════
   THE RULED FRAME
   -----------------------------------------------------------------------
   Figma rules the page with one vertical line down each side of the content
   column, running the full height of the document, and a horizontal divider
   between every section. The side rails are FIXED, not per-section: a rail
   that stops and restarts at every boundary reads as a series of ticks. One
   continuous line is the whole effect.
   ═══════════════════════════════════════════════════════════════════════ */
.rails {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: min(100% - var(--inset) * 2, var(--frame));
    pointer-events: none;
    z-index: 0;
}

.rails i {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--rail);
}

.rails i:nth-child(1) { left: 0; }
.rails i:nth-child(2) { left: 100%; margin-left: -1px; }

.frame {
    position: relative;
    z-index: 1;
    width: min(100% - var(--inset) * 2, var(--frame));
    margin: 0 auto;
}

/* A band is one horizontal slice of the page; its bottom hairline is the
   section divider. scroll-margin keeps the navbar from sitting on top of a
   heading when one of its links jumps here. */
.band {
    position: relative;
    border-bottom: 1px solid var(--rail);
    scroll-margin-top: var(--nav-h);
}

/* ═══════════════════════════════════════════════════════════════════════
   NAVBAR
   Figma: 84px tall, the content column ruled on both sides, a translucent
   near-black ground with a 14px backdrop blur.
   ═══════════════════════════════════════════════════════════════════════ */
.nav {
    position: sticky;
    top: 0;
    z-index: 40;
    background: color-mix(in srgb, var(--surface-page) 82%, transparent);
    backdrop-filter: saturate(160%) blur(14px);
    -webkit-backdrop-filter: saturate(160%) blur(14px);
    border-bottom: 1px solid var(--rail);
}

/**
 * THE RULES RUN THROUGH THE NAVBAR TOO.
 *
 * The page's side rails are painted by a fixed layer at z-index 0, so the
 * sticky navbar — which needs its own opaque background to be readable over
 * scrolling content — covers them, and the two lines appear to start below
 * the header. Re-drawn here as borders on the bar's own container, they land
 * on exactly the same pixels, and the rules read as one unbroken pair from
 * the top of the window to the bottom of the document.
 */
.nav-inner {
    width: min(100% - var(--inset) * 2, var(--frame));
    margin: 0 auto;
    height: var(--nav-h);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 var(--gutter);
    border-left: 1px solid var(--rail);
    border-right: 1px solid var(--rail);
}

/**
 * Three columns, and every child is PLACED rather than flowed. The section
 * links sit in the middle column, so they are centred on the bar itself and
 * not on whatever is left over beside the wordmark. Explicit columns are what
 * keeps the right-hand group on the right once the links are hidden on a
 * narrow screen: left to flow, two items in a three-column grid would put the
 * second one in the middle.
 */
.nav-inner {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
}

/* The section links. Secondary buttons, not underlined text: they do the same
   kind of thing the CTA does, one step quieter, so they get the same box at a
   lower contrast rather than a different species of control. */
.nav-links {
    grid-column: 2;
    justify-self: center;
    display: flex;
    align-items: center;
    gap: 2px;
}

.brand {
    grid-column: 1;
    justify-self: start;
    display: inline-flex;
    align-items: center;
    text-decoration: none;
    border-radius: var(--radius-control);
    transition: filter 150ms, opacity 150ms;
}

/**
 * The wordmark is an image, so the hover cannot be a colour change. On dark,
 * dimming it IS darkening it. On light the ink is already near-black and
 * cannot go darker in any way a reader would notice, so there it fades
 * instead: same affordance, the only direction each theme has left.
 */
.brand:hover {
    filter: brightness(0.78);
}
:root[data-theme='light'] .brand:hover {
    filter: none;
    opacity: 0.66;
}

.brand img {
    height: 29px;        /* Figma 35px mark ÷ 1.2 */
    width: auto;
}

/* One wordmark per theme. Dark is the default, so the light file is the one
   that hides. */
.logo-on-light {
    display: none;
}
:root[data-theme='light'] .logo-on-dark {
    display: none;
}
:root[data-theme='light'] .logo-on-light {
    display: block;
}

.nav-right {
    grid-column: 3;
    justify-self: end;
    display: flex;
    align-items: center;
    gap: 14px;
}

/* ── Compact nav (tablet and phone) ──────────────────────────────────────
   Below 1100px the section links already had nowhere to go; this is that
   same break extended to the theme and language controls too, so one menu
   replaces all three rather than leaving tablet widths with no way back to
   Problem/Solution/FAQ at all. Hidden by default (desktop shows the real
   controls inline); the 1100px query below switches both. */
.nav-burger {
    display: none;
    position: relative;
    width: 37px;
    height: 37px;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: transparent;
    border: 1px solid var(--border-control);
    border-radius: var(--radius-control);
    color: var(--ink-tertiary);
    cursor: pointer;
    transition: background-color 150ms, color 150ms, border-color 150ms;
}
.nav-burger:hover {
    background: var(--hover-fill-light);
    color: var(--ink-primary);
}

/* TWO LINES, NOT THREE, so the X they turn into reads clean rather than
   leaving a stray third stroke to hide. Both bars sit on the button's own
   centre and are pushed apart by a translateY offset at rest; opening the
   menu is just dropping that offset and rotating each bar onto the
   diagonal, one transition, no icon swap. */
.nav-burger-bar {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 17px;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
    transform: translate(-50%, -50%) translateY(var(--bar-offset)) rotate(0deg);
    transition: transform 280ms cubic-bezier(0.65, 0, 0.35, 1);
}
.nav-burger-bar:nth-child(1) {
    --bar-offset: -3.5px;
}
.nav-burger-bar:nth-child(2) {
    --bar-offset: 3.5px;
}
.nav-burger[aria-expanded="true"] .nav-burger-bar:nth-child(1) {
    transform: translate(-50%, -50%) rotate(45deg);
}
.nav-burger[aria-expanded="true"] .nav-burger-bar:nth-child(2) {
    transform: translate(-50%, -50%) rotate(-45deg);
}

/* The dropdown itself. A plain sibling of .nav-inner inside <header class="nav">,
   which is already position:sticky and so already the containing block this
   needs — no extra positioning context to add. [hidden] does the showing and
   hiding; the transition is opacity/transform only, both of which still
   animate under reduced motion being the one exception disabled wholesale,
   same as every other menu-style affordance on this page. */
.mobile-menu {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 39;
    background: var(--surface-page);
    border-bottom: 1px solid var(--rail);
    box-shadow: var(--shadow-dropdown);
    padding: 10px var(--gutter) 16px;
    display: flex;
    flex-direction: column;
}

/* The element's own `display: flex` above otherwise beats the UA
   stylesheet's `[hidden] { display: none }`, since both are author-origin
   rules of the same specificity and this one is declared later — which is
   exactly the bug that shipped first (the panel open by default, with
   nothing but JS ever setting `hidden` to close it). This has to win. */
.mobile-menu[hidden] {
    display: none;
}

.mobile-menu-link {
    padding: 13px 2px;
    font-size: 15px;
    font-weight: 500;
    color: var(--ink-secondary);
    text-decoration: none;
    border-bottom: 1px solid var(--divider-row);
}
.mobile-menu-link:hover {
    color: var(--ink-primary);
}

/* Language, then theme, side by side rather than stacked — one row, one
   divider off the links above it, nothing below. Each column reuses the
   desktop control wholesale (.lang-switch, .theme-btn) under its own short
   label, instead of inventing a mobile-only variant. */
.mobile-menu-toggles {
    display: flex;
    gap: 28px;
    margin-top: 14px;
    padding: 13px 2px 2px;
    border-top: 1px solid var(--divider-row);
}

.mobile-menu-toggle {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.mobile-menu-toggle-label {
    font-size: 15px;
    font-weight: 500;
    color: var(--ink-secondary);
}

/* ── Controls ──────────────────────────────────────────────────────────────
   Figma's CTA is 44px tall, 18px medium label, 8px radius, 24px of side
   padding. Divided by the 1.2 scale that is 37px and 15px, which is also
   close enough to Echo's control rhythm to sit next to the product. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 37px;
    padding: 0 20px;
    border: 1px solid transparent;
    border-radius: var(--radius-control);
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    line-height: 1;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 150ms, border-color 150ms, color 150ms, transform 150ms;
}

.btn-primary {
    background: var(--primary);
    color: var(--on-primary);
}
.btn-primary:hover {
    background: var(--primary-hover);
}

/* The large CTA is bigger in BOX, not in label: same 14px text, more air
   around it. A marketing page's main button has to be hittable; it does not
   have to shout in a second type size. */
.btn-lg {
    height: 46px;
    padding: 0 26px;
}

.btn-quiet {
    background: transparent;
    color: var(--ink-tertiary);
}
.btn-quiet:hover {
    background: var(--hover-fill-light);
    color: var(--ink-primary);
}

.btn-ghost {
    background: transparent;
    color: var(--ink-secondary);
    border-color: var(--border-control);
}
.btn-ghost:hover {
    background: var(--hover-fill-light);
    color: var(--ink-primary);
}

.btn:disabled {
    cursor: default;
    opacity: 0.8;
}

/* Theme toggle: one button, two states, swapped on the spot. 37px square on
   the CTA's radius, so the two controls beside each other read as one set
   rather than as a pill next to a circle. */
.theme-btn {
    width: 37px;
    height: 37px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: transparent;
    border: 1px solid var(--border-control);
    border-radius: var(--radius-control);
    color: var(--ink-tertiary);
    cursor: pointer;
    transition: background-color 150ms, color 150ms, border-color 150ms;
}
.theme-btn:hover {
    background: var(--hover-fill-light);
    color: var(--ink-primary);
}
.theme-btn svg {
    width: 17px;
    height: 17px;
}

/* The language switch: both labels visible at once, like a real two-way
   switch rather than the theme button's "glyph names the state you'd get"
   convention. Same outer box height as the theme button so the three
   nav-right controls sit on one baseline; the pill is the DISABLED half
   (see the [data-lang-set] markup), since the point of disabling the
   active side is exactly this: it reads as "pressed", not clickable. */
.lang-switch {
    display: inline-flex;
    align-items: center;
    height: 37px;
    padding: 3px;
    gap: 2px;
    border: 1px solid var(--border-control);
    border-radius: var(--radius-control);
}

.lang-switch-opt {
    all: unset;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 28px;
    height: 100%;
    padding: 0 7px;
    /* `all: unset` resets line-height to the font's own "normal", which
       on Geist leaves more room below the baseline than above it — the
       label then sits visibly high even though the box itself is
       perfectly centred. A line-height of 1 removes that lean. */
    line-height: 1;
    border-radius: calc(var(--radius-control) - 3px);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--ink-faint);
    cursor: pointer;
    transition: background-color 150ms, color 150ms;
}

.lang-switch-opt:hover:not(:disabled) {
    color: var(--ink-primary);
}

.lang-switch-opt:disabled {
    background: var(--hover-fill-light);
    color: var(--ink-primary);
    cursor: default;
}

/* Two glyphs, one per theme. Light shows the moon (what the click will give
   you), dark shows the sun. No third state: the toggle is a switch. */
.theme-btn .g-sun {
    display: none;
}
:root[data-theme='dark'] .theme-btn .g-sun {
    display: block;
}
:root[data-theme='dark'] .theme-btn .g-moon {
    display: none;
}

/* ═══════════════════════════════════════════════════════════════════════
   TYPE
   -----------------------------------------------------------------------
   The scale is set, not derived: body 16, h1 64, hero subtitle 20, h2 32,
   eyebrow 14, buttons 14, FAQ questions 18, footer 13. The clamps only say
   how each heading gets DOWN to a phone from those ceilings; nothing scales
   past them on a wide screen.
   ═══════════════════════════════════════════════════════════════════════ */
.eyebrow {
    font-family: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;
    font-size: 14px;
    font-weight: 500;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--primary-ink);
}

.h1 {
    font-size: clamp(2.25rem, 5.2vw, 4rem);     /* ceiling 64px */
    line-height: 1.05;
    letter-spacing: -0.035em;
    font-weight: 560;
    text-wrap: balance;
}

.h2 {
    font-size: clamp(1.625rem, 2.6vw, 2rem);    /* ceiling 32px */
    line-height: 1.15;
    letter-spacing: -0.028em;
    font-weight: 560;
    text-wrap: balance;
}

/* Not one of the given sizes: with h2 down at 32 a 23px card title crowded
   it, so the card titles sit at 20 to keep the step between them readable.
   One line to change if you want them back up. */
.h3 {
    font-size: 1.25rem;
    font-weight: 560;
    letter-spacing: -0.016em;
    line-height: 1.25;
}

.lede {
    font-size: 1.25rem;                          /* 20px */
    line-height: 1.5;
    color: var(--ink-tertiary);
    max-width: 30ch;
    text-wrap: pretty;
}

.copy {
    color: var(--ink-tertiary);
    text-wrap: pretty;
}

.muted {
    color: var(--ink-tertiary);
}

/* ═══════════════════════════════════════════════════════════════════════
   HERO
   -----------------------------------------------------------------------
   Headline, subtitle, one button. Nothing else: the hero makes the claim and
   the sections underneath carry the proof.

   The twinkle field is the background layer, full bleed across the viewport
   rather than inside the frame, so the rails cross it. It runs dense at the
   bottom and fades to nothing at the top, which reads as a horizon at dusk
   and leaves the type on calm ground.
   ═══════════════════════════════════════════════════════════════════════ */
.hero {
    position: relative;
}

/* The field is absolutely positioned against the container and clipped by
   it, so it stops at the rules instead of crossing them. Nothing on this
   page lives in the page margin. */
.hero .frame {
    overflow: hidden;
}

/**
 * THE FIELD IS A HORIZON, NOT A WALLPAPER.
 *
 * Full bleed across the viewport's WIDTH, but only the lower half of the
 * hero's height. Echo tunes the twinkle DENSITY per cell per second, so a
 * field's liveliness scales with its area: the constant that reads as a calm
 * shimmer on a 630-cell card reads as static over a 25,000-cell hero. And a
 * cell in the top rows can still twinkle to 0.5 on its own, which over a
 * full-height hero puts half-lit pixels directly behind the headline. Both
 * are fixed by bounding the field, not by retuning TwinkleField: those
 * constants are Echo's and this page does not get to move them.
 */
.hero-field {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    /* Was 72%: the blank strip above the field is exactly 100% minus this
       number, so this is the one knob that sets it directly. 75% leaves a
       quarter of the hero solid, not a third. */
    height: 75%;
    pointer-events: none;
    z-index: 0;
}

.hero-field canvas {
    width: 100%;
    height: 100%;
    opacity: 0.88;
    /* The fade-in itself also starts a little sooner within the field's own
       box (34% to 38%), so the field is visibly alive closer to the top of
       that quarter rather than only catching up well past it. */
    -webkit-mask-image: linear-gradient(to top, #000 0%, #000 38%, transparent 100%);
    mask-image: linear-gradient(to top, #000 0%, #000 38%, transparent 100%);
}

/* A single soft violet wash under the mosaic, so the field is bedded into the
   page instead of floating on flat black. */
.hero-glow {
    position: absolute;
    left: 50%;
    bottom: -22%;
    transform: translateX(-50%);
    width: 100%;
    height: 92%;
    pointer-events: none;
    background: radial-gradient(
        ellipse at 50% 100%,
        color-mix(in srgb, var(--primary) 26%, transparent) 0%,
        color-mix(in srgb, var(--primary) 10%, transparent) 42%,
        transparent 72%
    );
    z-index: 0;
}

.hero-inner {
    position: relative;
    z-index: 1;
    /**
     * THE HERO IS THE FIRST SCREEN, ALL OF IT. Exactly the viewport minus the
     * bar above it, so the next chapter starts one scroll down and never
     * peeks over the fold. dvh, not vh: on a phone the browser's own chrome
     * expands and collapses as you scroll, and vh would leave the divider
     * showing at rest.
     */
    padding: clamp(84px, 14vh, 150px) var(--gutter) clamp(96px, 16vh, 170px);
    /* Minus the navbar, and minus the two hairlines: the bar's bottom border
       and this section's own, so the divider lands exactly on the fold
       instead of one pixel under it. */
    min-height: calc(100dvh - var(--nav-h) - 2px);
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* Centered hero: title, lede and CTA share one centre line. */
    align-items: center;
    text-align: center;
    gap: 26px;
}

.hero-inner .h1 {
    /* Unbounded by the 14ch line box while centred: the headline wraps by
       its own measure, not by a column edge that no longer exists. */
    max-width: none;
}

.hero-cta {
    margin-top: 10px;
}

/* ═══════════════════════════════════════════════════════════════════════
   STORY SECTIONS
   -----------------------------------------------------------------------
   Problem, solution, value. One shape, used three times: a text column on
   the left (eyebrow, heading, paragraph) and its evidence on the right. The
   repetition is the point, it is what makes the three read as one argument
   rather than three unrelated blocks.
   ═══════════════════════════════════════════════════════════════════════ */
/**
 * ONE SHAPE, THREE TIMES, AT ONE HEIGHT.
 *
 * Text column on the left, a panel of --media-col on the right. The panel
 * carries NO padding of its own and the grid has no gap, so it runs to the
 * full height of the chapter and all the way out to the right-hand rule:
 * top, right and bottom aligned, with the section dividers as its other two
 * edges. The breathing room between the two columns is the text column's own
 * padding, not a gap, because a gap would pull the panel off the rule.
 *
 * --story-h on all three is what makes them read as a series. Figma draws
 * them at 780, 823 and 821; the eye reads that as three near-misses rather
 * than as a rhythm, so they are levelled here.
 */
.story {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--media-col);
    align-items: stretch;
    min-height: var(--story-h);
}

.story-text {
    align-self: center;
    display: flex;
    flex-direction: column;
    gap: 18px;
    max-width: 46ch;
    padding: clamp(48px, 5vw, 76px) var(--gutter);
}

.story-text .h2 {
    margin-top: 2px;
}

.story-media {
    position: relative;
    min-width: 0;
    overflow: hidden;      /* the panel is what crops its contents */
}

/**
 * THE PHOTOGRAPH IS THE PANEL.
 *
 * No frame, no radius, no border: it fills its cell edge to edge and is
 * cropped to fit by object-fit, so the chapter's own dividers and the
 * right-hand rule are the only lines around it. A rounded card inside a
 * ruled grid was two framing systems arguing.
 */
.media-photo {
    width: 100%;
    height: 100%;
    background: var(--surface-media);
}

.media-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The devices stand on a panel rather than on the page, which is what lifts
   them off a flat black background. Same box as the photograph, so the three
   chapters line up on both edges. */
.media-panel {
    width: 100%;
    height: 100%;
    background: var(--surface-media);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(26px, 3vw, 46px);
}

/**
 * THE ZOOM AFFORDANCE.
 *
 * `.is-zoomable` only goes on the two chapters where the detail genuinely
 * needs a closer look — the dashboard's small print and the phone's app
 * UI — not on the problem photo, which has nothing to read.
 *
 * The trigger is one <button> covering the whole panel, not just the device,
 * because "hover the right-hand side" is the brief: padding and background
 * are part of the clickable area, not only the pixels of the mockup. It
 * sits above the device in z-order so a click anywhere lands on it rather
 * than on the video or the frame image underneath.
 */
.media-panel.is-zoomable {
    position: relative;
}

.zoom-trigger {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    background: transparent;
    border: 0;
    cursor: zoom-in;
}

/* A slight dim on the whole panel, not just under the badge, so the hover
   state reads as "this area" rather than "this one small label". Darkening
   works the same direction in both themes against either a light dashboard
   shot or a dark phone mockup. */
.zoom-trigger::before {
    content: "";
    position: absolute;
    inset: 0;
    background: rgb(0 0 0 / 0);
    transition: background-color 160ms;
}

.zoom-trigger:hover::before,
.zoom-trigger:focus-visible::before {
    background: rgb(0 0 0 / 0.14);
}

.zoom-trigger:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

/* A fixed dark pill regardless of theme: this badge has to stay legible
   sitting on top of whatever photo or UI happens to be under the cursor,
   which rules out a token that only suits the page's own surface. */
.zoom-trigger-badge {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 9px 15px 9px 11px;
    border-radius: 999px;
    background: rgb(15 15 19 / 0.74);
    color: #fff;
    font-size: 13px;
    font-weight: 560;
    letter-spacing: -0.005em;
    opacity: 0;
    transform: translateY(5px) scale(0.96);
    transition: opacity 160ms, transform 160ms;
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
}

.zoom-trigger-badge svg {
    width: 15px;
    height: 15px;
}

.zoom-trigger:hover .zoom-trigger-badge,
.zoom-trigger:focus-visible .zoom-trigger-badge {
    opacity: 1;
    transform: none;
}

/* ── the zoom lightbox itself ─────────────────────────────────────────
   A second <dialog>, for the same reason the contact modal is one: ESC, the
   focus trap and the inert background come free from the platform, and the
   backdrop-click-to-close handler is a near copy of the one already written
   for it. The difference is what is inside — instead of a form, the exact
   .media-device node clicked is MOVED here (not cloned), so a playing video
   keeps playing without a restart, and moved back on close. */
/* `.modal.zoom-modal`, not `.zoom-modal` alone: the base `.modal` rule
   earlier in this file sets its own width/background/border/box-shadow at
   the exact same specificity, and a later rule of EQUAL specificity wins the
   cascade regardless of which one looks more specific to a reader. That was
   a real, shipped bug — every property below was silently lost to `.modal`'s
   defaults, which is where the card-like frame and the pinned 560px width
   around the "zoomed" image were coming from. Two classes beats one, so
   this now wins no matter where either rule sits in the file.

   WIDTH IS fit-content, NOT A FIXED BUDGET. The dialog hugs whatever the
   media inside actually renders at (capped by max-width so it can't blow out
   a wide screen), so the close button pinned to its corner always sits
   right next to the media instead of floating off in empty transparent
   space the media never grew into. */
.modal.zoom-modal {
    width: fit-content;
    max-width: min(96vw, 1400px);
    background: transparent;
    border: none;
    box-shadow: none;
}

/* Position and a default icon colour only — size and the hover fill both
   come from the shared .modal-close rule now. The colour override earns its
   keep: unlike the other two dialogs, this one sits on arbitrary page
   content (a photo, a light screenshot) rather than the modal's own themed
   surface, so the default --ink-quaternary token has no reliable contrast
   guarantee here. White does, against the backdrop blur. */
.zoom-modal .modal-close {
    position: absolute;
    top: 14px;
    right: 14px;
    z-index: 1;
    color: #fff;
}

/* The phone sits in the stage with no margin around it, so the button's
   usual 14px inset lands on the glass itself rather than beside it. Pulled
   fully outside the device's own box here, onto the blurred backdrop. Pulled
   back to the normal inset on a narrow phone, see the 640px query below,
   where there is no spare width to push it into. */
.zoom-modal--phone .modal-close {
    top: -20px;
    right: -20px;
}

.zoom-stage {
    display: flex;
    align-items: center;
    justify-content: center;
    max-height: calc(100dvh - 32px);
    overflow: auto;
}

/* Dashboard-only, set by zoom.js from the trigger's own data-zoom-caption.
   Sits below the stage rather than inside it, on the same blurred backdrop
   as the close button, so it reads as a caption for the whole lightbox
   rather than as part of the screenshot. */
/* Same font-size and line-height as .copy (inherited from body: 16px/1.6) —
   this was visibly a different, smaller text style before. Still centered
   and still a fixed light colour rather than the --ink-tertiary token .copy
   itself uses: this caption sits on the modal's always-dark blurred backdrop
   regardless of site theme (see .modal::backdrop's --scrim, which is dark in
   both themes), where a light-theme ink colour would go unreadable. */
.zoom-caption {
    margin: 18px auto 0;
    max-width: 74ch;
    text-align: center;
    font-size: 1rem;
    line-height: 1.6;
    color: rgba(255, 255, 255, 0.82);
}

/* Bigger than the in-page shell, same proportions: the selector below only
   adds a height, so every other rule that already sizes and shadows this
   device keeps applying unchanged wherever the node happens to sit. */
.zoom-stage .media-device.is-phone-video .device-shell {
    height: min(84vh, 880px);
}

/* The in-page dashboard shot is sized by `width: 100%` of the story panel it
   sits in (see .is-screen above) — a percentage that, once the device is
   moved into a stage with no width of its own, has nothing definite left to
   resolve against and quietly collapses back to roughly its in-page size.
   That was the second half of "the zoom doesn't really zoom": the card bug
   capped the dialog at 560px, and this collapse meant the image stayed
   small even once the dialog could grow. An explicit, definite width breaks
   both: comfortably bigger than before, capped so it still fits a phone. */
.zoom-stage .media-device.is-screen img {
    width: min(92vw, 1150px);
    max-height: calc(100dvh - 32px);
}

/* The laptop is a whole machine now rather than a shot that bled off the
   right edge, so it is centred like the phone. It gets tighter padding than
   the default panel because the frame is wide and shallow: at the shared
   padding a full laptop sits marooned in the middle of a 660px panel. */
.media-panel.is-wide {
    padding: clamp(22px, 2.6vw, 40px);
}

/**
 * A DEVICE MOCKUP GETS NO FRAME OF ITS OWN.
 *
 * The phone and the laptop are transparent cutouts carrying their own soft
 * shadow. A border around either would fight that shadow, and the device
 * would stop reading as an object standing on the panel and start reading as
 * a picture of one.
 *
 * The phone is deliberately the smaller of the two: it is a phone.
 */
.media-device {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    max-width: 100%;
}

/* flex: none is the whole trick. As a shrinkable flex item the mockup is
   squeezed to the column's width first and only then capped by max-height,
   which ends with a letterboxed image inside a box far bigger than the
   device. Taken out of the flex sizing it keeps its own ratio and the cap
   means what it says. */
.media-device img,
.media-device video {
    flex: none;
    width: auto;
    height: auto;
    max-height: 500px;
    max-width: 100%;
    object-fit: contain;
}

.media-device.is-phone img,
.media-device.is-phone video {
    max-height: 470px;              /* Figma's 593 ÷ 1.2: a phone, not a poster */
}

/**
 * THE PHONE IS TWO LAYERS, NOT ONE SCREENSHOT.
 *
 * `.device-shell` is sized directly (the img/video rule above does not match
 * a plain <div>) and carries the frame's own ratio, 1410:2896, so it scales
 * as one unit at any height. Inside it:
 *   - `.device-video` is positioned at the screen cutout's exact percentage
 *     of the frame — 4.3972/1.8301 from the top-left, 91.2057% x 96.3398% —
 *     measured from the PNG's own alpha channel, not eyeballed.
 *   - `.device-frame` sits on top at 100%/100%, pointer-events excluded so
 *     the video can still receive taps if it ever needs controls.
 *
 * The radius on the video is not decorative. All four corners of the screen
 * cutout fall OUTSIDE the device's silhouette (verified against the source
 * file), so a square video corner would print past the glass. 12.986% /
 * 5.986% is that same corner radius expressed as a fraction of the video
 * box's own width and height — CSS resolves a percentage border-radius
 * against each axis separately, which is exactly what a non-square box with
 * one circular (in px) corner radius needs.
 *
 * Bigger than the old flat shot on purpose — a playing preview needs to read
 * at a glance, where a static screenshot could be smaller.
 */
.media-device.is-phone-video .device-shell {
    position: relative;
    flex: none;
    /* Same curve as before, scaled by 0.8 so it tops out around 480px
       instead of 600px. The panel centres it either way, so this only
       changes size, not position. */
    height: clamp(336px, 35.2vw, 480px);
    aspect-ratio: 1410 / 2896;
}

.device-shell .device-video,
.device-shell .device-frame {
    position: absolute;
    display: block;
    /* The generic `.media-device img, .media-device video` rule above pins
       every mockup to max-height:500px/max-width:100% so a flex-sized device
       never outgrows its panel. Both layers here are already pinned exactly
       by position:absolute + percentage width/height against the shell, so
       that cap only gets in the way: it was clamping the frame to 500px
       tall inside a 600px shell, which left it short of the shell's own
       edges and is exactly the misalignment this neutralises. */
    max-height: none;
    max-width: none;
}

.device-shell .device-frame {
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

.device-shell .device-video {
    left: 4.3972%;
    top: 1.8301%;
    width: 91.2057%;
    height: 96.3398%;
    object-fit: cover;
    border-radius: 12.986% / 5.986%;
}

/* Drawn rather than baked, same reasoning as the laptop in the value
   chapter: `drop-shadow` on the shell shadows the union of the video and the
   frame on top of it, so it traces the phone's silhouette and follows
   whichever panel it is sitting on instead of assuming one background. */
.media-device.is-phone-video .device-shell {
    filter:
        drop-shadow(0 1px 1px rgb(0 0 0 / 0.17))
        drop-shadow(0 9px 15px rgb(0 0 0 / 0.13))
        drop-shadow(0 26px 44px rgb(0 0 0 / 0.17));
}

:root[data-theme='dark'] .media-device.is-phone-video .device-shell {
    filter:
        drop-shadow(0 2px 2px rgb(0 0 0 / 0.55))
        drop-shadow(0 12px 20px rgb(0 0 0 / 0.45))
        drop-shadow(0 32px 54px rgb(0 0 0 / 0.5));
}

/**
 * THE SCREEN IS SIZED BY WIDTH, AND ITS SHADOW IS DRAWN RATHER THAN BAKED.
 *
 * Width, because the shot is wide and shallow: the height cap that keeps the
 * phone honest would leave this one floating at half the panel's width.
 *
 * Drawn, because `drop-shadow` follows the ALPHA CHANNEL, so it picks up the
 * rounded corners of the frame exactly instead of squaring them off the way
 * a `box-shadow` on the <img> box would. It also means the shadow suits
 * whichever panel it is sitting on, which a shadow exported into the PNG
 * never can: that is what went wrong with the previous file, which carried
 * its own and showed as a grey smudge on the light panel and a muddy band
 * on the dark one.
 *
 * Three passes, because a single one never looks real — a tight one holding
 * the edge down, a mid lift, and a wide ambient. This only works while the
 * source stays free of a shadow of its own; bake one back in and these
 * trace the haze as well.
 */
.media-device.is-screen img,
.media-device.is-screen video {
    width: 100%;
    max-height: none;
    filter:
        drop-shadow(0 1px 1px rgb(0 0 0 / 0.17))
        drop-shadow(0 9px 15px rgb(0 0 0 / 0.13))
        drop-shadow(0 26px 44px rgb(0 0 0 / 0.17));
}

/* On the dark panel the ground is nearly as dark as the frame itself, so a
   light-mode shadow simply vanishes. Deeper and tighter to compensate. */
:root[data-theme='dark'] .media-device.is-screen img,
:root[data-theme='dark'] .media-device.is-screen video {
    filter:
        drop-shadow(0 2px 2px rgb(0 0 0 / 0.55))
        drop-shadow(0 12px 20px rgb(0 0 0 / 0.45))
        drop-shadow(0 32px 54px rgb(0 0 0 / 0.5));
}

/* ═══════════════════════════════════════════════════════════════════════
   AUDIENCES
   Three cards in a row, ruled apart, each with a violet icon tile.
   ═══════════════════════════════════════════════════════════════════════ */
.aud-head {
    padding: clamp(52px, 6vw, 86px) var(--gutter) clamp(30px, 3.4vw, 46px);
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.aud-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    border-top: 1px solid var(--rail);
}

.aud-card {
    padding: clamp(32px, 3.4vw, 50px) var(--gutter);
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
    transition: background-color 240ms;
}

.aud-card + .aud-card {
    border-left: 1px solid var(--rail);
}

/* The same quiet panel tone the solution/value media sit on, one step
   darker, so the hovered card reads as "lit" without competing with the
   icon tile's own glow. */
.aud-card:hover {
    background: color-mix(in srgb, var(--surface-media) 92%, black 8%);
}

.aud-icon {
    width: 67px;            /* Figma 80 ÷ 1.2 */
    height: 67px;
    border-radius: var(--radius-card);
    background: var(--primary-soft);
    color: var(--primary-ink);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 6px;
    transition: background-color 240ms, color 240ms, box-shadow 240ms;
}

.aud-icon svg {
    width: 34px;
    height: 34px;
}

/* Only the icon tile's fill, glow and colour invert on hover; it does not
   scale, so the gesture reads as "this icon is alight" rather than as a
   button growing under the cursor. */
.aud-card:hover .aud-icon {
    background: var(--primary);
    color: var(--on-primary);
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 45%, transparent),
        0 10px 28px color-mix(in srgb, var(--primary) 38%, transparent);
}

/* ═══════════════════════════════════════════════════════════════════════
   FAQ
   -----------------------------------------------------------------------
   Heading on the left, the questions on the right, exactly as Figma has it.
   Each row is a <details>, so the open/closed state, the keyboard and the
   screen-reader announcement come from the platform rather than from script.
   ═══════════════════════════════════════════════════════════════════════ */
.faq {
    display: grid;
    grid-template-columns: 1fr 1.72fr;
    gap: clamp(28px, 4vw, 64px);
    padding: clamp(56px, 6vw, 92px) var(--gutter);
    align-items: start;
}

.faq-side {
    display: flex;
    flex-direction: column;
    gap: 14px;
    position: sticky;
    top: calc(var(--nav-h) + 40px);
}

.faq-item {
    border-top: 1px solid var(--rail);
}

.faq-item:last-child {
    border-bottom: 1px solid var(--rail);
}

.faq-q {
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    padding: 20px 0;
    cursor: pointer;
    font-size: 1.125rem;                         /* 18px */
    font-weight: 500;
    letter-spacing: -0.012em;
    color: var(--ink-primary);
    transition: color 150ms;
}

.faq-q::-webkit-details-marker {
    display: none;
}

.faq-q:hover {
    color: var(--primary-ink);
}

/* The plus answers the same hover as the question text, not just the open
   state: currentColor already draws it, so this is the one line it takes. */
.faq-q:hover .faq-plus {
    color: var(--primary-ink);
}

/* The plus. One glyph that rotates 45 degrees into a cross when the row
   opens, which is the same affordance Figma draws and costs one transform. */
.faq-plus {
    flex: none;
    width: 22px;
    height: 22px;
    position: relative;
    color: var(--ink-tertiary);
    transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1), color 150ms;
}

.faq-plus::before,
.faq-plus::after {
    content: '';
    position: absolute;
    background: currentColor;
    border-radius: 1px;
}
.faq-plus::before {
    left: 2px;
    right: 2px;
    top: 10px;
    height: 2px;
}
.faq-plus::after {
    top: 2px;
    bottom: 2px;
    left: 10px;
    width: 2px;
}

/**
 * WITHOUT SCRIPT the open attribute is the only signal there is, so it drives
 * the icon.
 */
.faq-item[open] .faq-plus {
    transform: rotate(45deg);
    color: var(--primary-ink);
}

.faq-item[open] .faq-q {
    color: var(--primary-ink);
}

/**
 * WITH SCRIPT a class leads instead, set on the click and cleared on the
 * click. `open` cannot do this job in both directions: it is set before the
 * expand so the cross arrives on time, but it survives until the collapse has
 * finished playing, which left the cross standing there for the length of the
 * animation and then snapping back. The class turns at the moment the reader
 * acts, in both directions.
 */
.js-faq .faq-item[open] .faq-plus {
    transform: none;
    color: var(--ink-tertiary);
}

.js-faq .faq-item[open] .faq-q {
    color: var(--ink-primary);
}

.js-faq .faq-item.is-open .faq-plus {
    transform: rotate(45deg);
    color: var(--primary-ink);
}

.js-faq .faq-item.is-open .faq-q {
    color: var(--primary-ink);
}

.faq-a {
    padding: 0 60px 22px 0;
    margin-top: -4px;
    color: var(--ink-tertiary);
    max-width: 68ch;
}

/* Multi-paragraph answers: only the last paragraph keeps the bottom
   padding, so the panel's collapse stays a pure height change. */
.faq-a + .faq-a {
    padding-bottom: 0;
    margin-top: 10px;
}

/* Inline call-to-action inside an FAQ answer ("get in touch" /
   "Kontaktieren Sie uns"). A real button so the contact modal opens from
   anywhere in the text; styled as a link that clearly contrasts with the
   grey answer copy: white in dark theme, ink-primary in light. */
.faq-link {
    background: none;
    border: none;
    margin: 0;
    padding: 0;
    font: inherit;
    line-height: inherit;
    text-align: inherit;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
    color: var(--ink-primary);
    cursor: pointer;
    transition: color 150ms;
}

:root[data-theme='dark'] .faq-link {
    color: #ffffff;
}

.faq-link:hover {
    color: var(--primary);
}

:root[data-theme='dark'] .faq-link:hover {
    color: var(--primary-ink);
}

/* The answer lives in a wrapper with nothing but a height, because that is
   the one box a script can animate cleanly: the padding stays on the
   paragraph inside it, so the collapse is a pure height change with no
   padding fighting it at the end. */
.faq-panel {
    overflow: hidden;
}

/* ═══════════════════════════════════════════════════════════════════════
   CLOSING CTA
   -----------------------------------------------------------------------
   The twinkle field returns here, in a band of its own above the footer:
   shorter than the hero's, so it reads as a reprise rather than a repeat,
   and bright enough that this is the thing the eye lands on at the end of
   the page.

   INSIDE THE CONTAINER, unlike the hero. The hero's field is the horizon
   the page opens on and is allowed to run the full width of the window; this
   one is a panel, and a panel that ignores the container rules stops being
   part of the grid. Its edges land exactly on them.
   ═══════════════════════════════════════════════════════════════════════ */
.cta {
    position: relative;
}

/* The field and the wash are absolutely positioned against THIS box, so the
   container is what clips them. */
.cta .frame {
    overflow: hidden;
}

.cta-field {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 72%;
    pointer-events: none;
    z-index: 0;
}

.cta-field canvas {
    width: 100%;
    height: 100%;
    opacity: 0.9;
    -webkit-mask-image: linear-gradient(to top, #000 0%, #000 30%, transparent 100%);
    mask-image: linear-gradient(to top, #000 0%, #000 30%, transparent 100%);
}

.cta-glow {
    position: absolute;
    left: 50%;
    bottom: -40%;
    transform: translateX(-50%);
    width: 100%;
    height: 130%;
    pointer-events: none;
    z-index: 0;
    background: radial-gradient(
        ellipse at 50% 100%,
        color-mix(in srgb, var(--primary) 22%, transparent) 0%,
        color-mix(in srgb, var(--primary) 8%, transparent) 45%,
        transparent 74%
    );
}

.cta-inner {
    position: relative;
    z-index: 1;
    padding: clamp(64px, 7vw, 104px) var(--gutter);
    min-height: 340px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 26px;
    text-align: center;
}

.cta-inner .h2 {
    max-width: 26ch;
}

/* ═══════════════════════════════════════════════════════════════════════
   FOOTER
   Brand on the left, Legal and Contact columns beside it, one ruled line,
   then the small print. No twinkle: the band above it has that job now.

   The rule above the footer stops at the container rules rather than running
   the width of the window, so the three lines close a box instead of
   crossing one.
   ═══════════════════════════════════════════════════════════════════════ */
.footer {
    position: relative;
    background: var(--surface-page);
}

/**
 * The footer is ruled on three sides: the container's own two lines carried
 * down to the end of the document, and the line the closing band sits on.
 * The fixed rail layer cannot do the vertical pair here — the footer needs an
 * opaque background of its own and paints over it — so they are drawn as
 * borders on this box, on exactly the same pixels.
 *
 * NO HORIZONTAL PADDING OF ITS OWN. Each row inside carries its own gutter
 * instead, which is what lets the line above the small print run the full
 * width of the container, rule to rule, rather than stopping short at the
 * text.
 */
.footer-inner {
    position: relative;
    z-index: 1;
    width: min(100% - var(--inset) * 2, var(--frame));
    margin: 0 auto;
    border-top: 1px solid var(--rail);
    border-left: 1px solid var(--rail);
    border-right: 1px solid var(--rail);
    padding: 0;
}

.footer-top {
    display: grid;
    grid-template-columns: 1fr auto auto auto;
    gap: clamp(32px, 6vw, 96px);
    align-items: start;
    padding: clamp(44px, 5vw, 68px) var(--gutter) clamp(40px, 5vw, 64px);
}

.footer-brand img {
    height: 26px;
    width: auto;
}

.footer-brand p {
    margin-top: 14px;
    font-size: 13px;
    /* Same idle tone as the footer link rows (Datenschutzerklärung, AGB, …),
       which use --ink-tertiary; the tagline sat a step darker before. */
    color: var(--ink-tertiary);
    max-width: 34ch;
}

.footer-col {
    display: flex;
    flex-direction: column;
    gap: 12px;
    min-width: 170px;
}

/* The three column headings (Legal, Contact, Language) are the brightest
   text in the footer on purpose, bold AND a size step above the rows
   under them, so they read as labels rather than as one more row.
   Colour alone (pure white, confirmed pixel-for-pixel, no overlay or
   opacity involved anywhere in the ancestor chain) was not enough
   separation at the same 13px the body rows use — size is the lever
   that actually reads as "clearly standing out". Pure white only in
   dark theme (the site's default): in light theme ink-primary is
   already the darkest ink there is, and literal white would vanish
   against the page. */
.footer-col h2 {
    font-size: 15px;
    font-weight: 700;
    color: var(--ink-primary);
    letter-spacing: -0.01em;
}
:root[data-theme='dark'] .footer-col h2 {
    color: #ffffff;
}

.footer-col a,
.footer-col span {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    color: var(--ink-tertiary);
    text-decoration: none;
    transition: color 150ms;
}

.footer-col a:hover {
    color: var(--ink-primary);
}

/* A link's own two-language spans (.lang-de/.lang-en) are themselves
   <span> elements, which would otherwise match the rule above and pin
   their own colour — overriding the inherited hover state from the <a>
   around them. This hands that colour back to the parent. Same fix,
   same reason, for the column headings: their lang spans were pinned
   to ink-tertiary by the same rule, which is the actual reason the
   "white" heading colour below never reached the visible text. */
.footer-col a span,
.footer-col h2 span {
    color: inherit;
}

.footer-col svg {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--ink-quaternary);
}

/* The two Language rows are actions, not links, so they are buttons rather
   than anchors; `all: unset` strips the user-agent button chrome down to
   the same baseline the surrounding <a> and <span> rows already share. */
.footer-col button {
    all: unset;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font: inherit;
    font-size: 13px;
    color: var(--ink-tertiary);
    cursor: pointer;
    transition: color 150ms;
}

.footer-col button:hover {
    color: var(--ink-primary);
}

/* The active language is read, not acted on, but stays the same colour as
   the inactive one: nothing marks it except that it does not respond to
   hover or click. The highlight is reserved for the option you'd actually
   switch to. */
.footer-col button:disabled {
    cursor: default;
}

.footer-bottom {
    border-top: 1px solid var(--rail);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    padding: 20px var(--gutter) 24px;
    font-size: 12px;
    color: var(--ink-quaternary);
}

/* Scoped to the footer: .num is shared with the figures elsewhere on the page,
   which must keep their own size. */
.footer-bottom .num {
    font-size: 11px;
}

/* The one line on this page that is not here to be read quickly. */
.footer-sdg {
    color: var(--primary);
    font-weight: 600;
}

/* ═══════════════════════════════════════════════════════════════════════
   CONTACT MODAL
   -----------------------------------------------------------------------
   A native <dialog> opened with showModal(), which is what gives ESC, the
   focus trap and the inert background for free and correctly.

   ONE FORM, nothing required. Figma's "Demo Widget 4" (SLH-Website node
   23:166): a branded aside beside a lead-capture form, collapsing to one
   column on phones. Submitting swaps the form for a success screen in the
   same dialog; see .done below.
   ═══════════════════════════════════════════════════════════════════════ */
.modal {
    width: min(100%, 560px);
    max-height: calc(100dvh - 32px);
    /* Explicit, not just relying on the <dialog> UA default: dead centre,
       both axes. The 640px rule further down overrides this one property
       to pin the sheet to the bottom edge instead. */
    margin: auto;
    padding: 0;
    border: 1px solid var(--border-default);
    border-radius: var(--radius-pop);
    background: var(--surface-page);
    color: var(--ink-primary);
    box-shadow: var(--shadow-compact-popover);
    overflow: visible;
}

.modal::backdrop {
    background: var(--scrim);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
}

/* The backdrop fades with the dialog instead of snapping in/out at full
   blur the instant showModal()/close() fires. Scoped to .modal-request:
   the zoom lightbox's backdrop is untouched. */
.modal.modal-request[open]::backdrop {
    animation: backdrop-in 220ms ease forwards;
}
.modal.modal-request.is-closing::backdrop {
    animation: backdrop-out 180ms ease forwards;
}

@keyframes backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes backdrop-out {
    from { opacity: 1; }
    to   { opacity: 0; }
}

.modal[open] {
    animation: modal-in 220ms cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes modal-in {
    from { opacity: 0; transform: translateY(8px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

/* THE EXIT, above the phone breakpoint: the same lift as modal-in, played
   backwards. contact.js adds this class just before dialog.close() (a
   <dialog> cannot animate its own removal) and removes it again on the
   next open. The 640px rule further down replaces this with the sheet
   slide for a phone-width screen. */
.modal.modal-request.is-closing {
    animation: modal-out 180ms cubic-bezier(0.4, 0, 1, 1) forwards;
}

@keyframes modal-out {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translateY(8px) scale(0.985); }
}

/* No inner scroll: the content (aside + form, side by side above 640px)
   fits inside the dialog's own max-height without it, so the dialog
   itself — centred by the <dialog> UA default, nothing here overrides
   its margin — is the only box, not a scrollable pane inside a fixed
   outer frame. The 640px rule further down keeps its own scroll cap;
   a phone's single-column stack is taller and still wants the safety
   net. */
.modal-scroll {
    position: relative;
    overflow: visible;
    border-radius: var(--radius-pop);
}

/* One close/X treatment everywhere on the site — the contact modal, the
   "Request a demo" success screen, and the zoom lightbox all use this exact
   button unchanged (bar one colour override on the zoom lightbox, below).
   32×32, distinct from the 37px theme toggle beside it elsewhere in the
   nav, with the same squared hover fill as the Problem/Solution/FAQ nav
   links (.btn-quiet) — a visible background on hover, not a circle, so it
   reads as the same kind of pressable control as everything else here. */
.modal-close {
    position: absolute;
    top: 18px;
    right: 18px;
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: transparent;
    border: none;
    border-radius: var(--radius-control);
    color: var(--ink-quaternary);
    cursor: pointer;
    z-index: 2;
    transition: background-color 150ms, color 150ms;
}
.modal-close:hover {
    background: var(--hover-fill-light);
    color: var(--ink-primary);
}
.modal-close svg {
    width: 15px;
    height: 15px;
}

/* Default-state contrast only — this one alone sits over two different
   backgrounds depending on viewport (the themed form panel, or the always-
   dark violet aside once the two stack on a phone), so the plain transparent
   resting state the other close buttons use is not reliably legible here; a
   quick zoom-in on a real screenshot is what caught it. --surface-page flips
   with the theme, so the tint reads against both the form panel and the
   aside, in either theme.

   The explicit :hover restates the shared rule verbatim rather than relying
   on it: `.modal-request .modal-close` and `.modal-close:hover` are EQUAL
   specificity (one class + one pseudo, two classes), and this rule comes
   later in the file, so without its own :hover it would win the cascade
   tie even while hovered and silently swallow the hover fill — exactly the
   bug a previous pass here already hit twice. Spelling it out instead of
   trusting source order is what keeps the hover identical to every other
   close button on the site, on purpose. */
.modal-request .modal-close {
    background: color-mix(in srgb, var(--surface-page) 55%, transparent);
}
.modal-request .modal-close:hover {
    background: var(--hover-fill-light);
    color: var(--ink-primary);
}

/* ── Two-column layout ───────────────────────────────────────────────────
   Figma "Demo Widget 4" (SLH-Website node 23:166), scaled ÷1.2 like every
   other size on this page. An always-dark branded aside (the brand violet
   is the same in both themes on purpose, see the token comment above) next
   to a form that follows the page's own theme. Collapses to one column on
   phones, aside first. ──────────────────────────────────────────────────── */
.request-demo {
    display: grid;
    grid-template-columns: minmax(0, 400px) minmax(0, 1fr);
}

.request-aside {
    background: radial-gradient(
        ellipse 130% 110% at 28% -10%,
        color-mix(in srgb, var(--primary) 70%, white) 0%,
        var(--primary) 32%,
        color-mix(in srgb, var(--primary) 38%, #26232f) 68%,
        #201e26 100%
    );
    color: #fff;
    padding: 32px 28px;
    display: flex;
    flex-direction: column;
    /* 64px, not space-between: the gap is fixed so the headline sits a set
       distance under the logo regardless of how tall the form column makes
       this panel, rather than drifting down to split whatever space is
       left over. */
    gap: 64px;
    border-radius: var(--radius-pop) 0 0 var(--radius-pop);
}

.request-aside-brand img {
    height: 32px;
    width: auto;
    display: block;
}

.request-aside-copy {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.request-aside-copy .h2 {
    color: #fff;
}

.request-aside-copy p {
    font-size: 14.5px;
    line-height: 1.6;
    color: rgba(255, 255, 255, 0.84);
}

.request-form {
    padding: 56px 26px 26px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* Wider than the base .modal: two columns need the room. Equal selector
   specificity to .modal's own width, and declared after it, so this wins.
   +100px over the previous 780px, matching the aside's own 300→400px
   widening 1:1 so the form column keeps the exact width it had before. */
.modal-request {
    width: min(100%, 880px);
}

.field {
    display: flex;
    flex-direction: column;
    gap: 7px;
}

.field > span {
    font-size: 14px;
    font-weight: 500;
    color: var(--ink-primary);
}

/* The asterisk on first name, last name and business e-mail: the only
   hint a reader gets before they submit that these three are the ones
   that actually gate the form. Same colour as the label it sits next
   to, not red: it marks a field, it is not an error. */
.field-req {
    color: inherit;
    margin-left: 2px;
}

/* Inputs are Echo's, one size up for a page that is read at arm's length. */
.input,
.textarea {
    width: 100%;
    font: inherit;
    font-size: 14.5px;
    color: var(--ink-primary);
    background: var(--surface-page);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-control);
    padding: 0 12px;
    height: 38px;
    transition: border-color 150ms;
}

.textarea {
    height: auto;
    min-height: 86px;
    padding: 9px 12px;
    line-height: 1.5;
    resize: vertical;
}

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

.input:focus,
.textarea:focus {
    outline: none;
    border-color: var(--primary);
}

.input[aria-invalid='true'] {
    border-color: var(--color-error-fg);
}

.two-up {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

/* The honeypot. Off-screen rather than display:none, because some bots skip
   fields that are not rendered at all. Never focusable, never announced. */
.hp {
    position: absolute;
    left: -9999px;
    top: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.submit-row {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 2px;
}

.submit-row .btn {
    width: 100%;
    height: 40px;
}

.privacy-line {
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--ink-quaternary);
}

.privacy-line a {
    color: var(--ink-tertiary);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.privacy-line a:hover {
    color: var(--ink-primary);
}

.err {
    display: none;
    align-items: flex-start;
    gap: 6px;
    font-size: 12.5px;
    color: var(--color-error-fg);
}

.err.on {
    display: flex;
}

.form-err {
    padding: 9px 11px;
    border-radius: var(--radius-control);
    background: var(--color-error-bg);
}

/* The submit label's idle and busy text are each written once per
   language and layered the same way the language pair itself is: a second
   attribute (.is-busy, on the dialog) picks one of two static spans, so
   nothing here is ever rewritten by script. */
.busy-text {
    display: none;
}
.is-busy .idle-text {
    display: none;
}
.is-busy .busy-text {
    display: inline;
}

/* Spinner inside the submit button. */
.spinner {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 1.6px solid color-mix(in srgb, var(--on-primary) 30%, transparent);
    border-top-color: var(--on-primary);
    animation: spin 700ms linear infinite;
    flex: none;
    display: none;
}

.is-busy .spinner {
    display: block;
}

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

/* The success state replaces the form inside the same modal. The user never
   leaves the page and never loses the context they opened it from.

   Figma "Demo Widget 8" (SLH-Website node 26:344): a checkmark that draws
   itself in, held inside a circle that borrows the comet's own "resting
   flame" idea from twinkle.js — a few pixels always catching and dying
   behind it — except this one radiates from a fixed centre rather than
   following a pointer, and fades out before it reaches the edge of its own
   canvas (see assets/js/success-field.js). .done-badge is the shared
   stacking context: the canvas is absolute and the checkmark is a static
   flex child with its own z-index, so the mark always paints above the
   field regardless of source order. */
.done {
    position: relative;
    display: none;
    flex-direction: column;
    align-items: center;
    max-width: 420px;
    margin: 0 auto;
}

.modal.is-done .request-demo {
    display: none;
}

.modal.is-done .done {
    display: flex;
}

/* Padding and the badge below are both deliberately smaller than an earlier
   pass: the success screen was running ~160px taller than the form it
   replaces, which made submitting read as the dialog suddenly growing
   rather than as one continuous widget. Shorter top padding also does the
   other half of that ask — it pulls the checkmark and the field up rather
   than centring them in a tall card. */
.done-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
    padding: 16px 26px 28px;
}

.done-badge {
    position: relative;
    width: 250px;
    height: 250px;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* width/height:100%, not just inset:0 — a <canvas> is a replaced element, and
   inset alone leaves a replaced element's size at its intrinsic default (the
   canvas's own width/height attributes) instead of stretching it, the same
   reason .hero-field and .cta-field canvases both state it explicitly. */
.done-field {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .done-field {
        display: none;
    }
}

.done-mark {
    position: relative;
    z-index: 1;
    width: 64px;
    height: 64px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--primary) 14%, var(--surface-page));
    border: 2px solid var(--primary);
    color: var(--primary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.done-mark svg {
    width: 28px;
    height: 28px;
}

/* The checkmark draws itself in rather than appearing already finished.
   32 comfortably exceeds this exact path's length (getTotalLength() reports
   ~22.6 in the icon's own 24px viewBox), so the dash both starts and ends
   fully hidden/fully drawn with no visible remainder. */
.done-check {
    stroke-dasharray: 32;
    stroke-dashoffset: 32;
    animation: done-check-draw 460ms 160ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

@keyframes done-check-draw {
    to {
        stroke-dashoffset: 0;
    }
}

/* Scoped to the subcopy specifically, not the generic `.done p`: that
   selector (class + type) is MORE specific than the title's own `.h2`, and
   would have silently won the font-size back down to 14.5px on the title
   too — the exact cascade trap already found once in this file. */
.done [data-done-sub] {
    font-size: 14.5px;
    color: var(--ink-tertiary);
    max-width: 38ch;
}

/* Wide and primary, matching the form's own submit button rather than a
   quiet ghost dismissal — this is the one action the success screen offers. */
.done-cta {
    width: 100%;
    margin-top: 8px;
}

/* ═══════════════════════════════════════════════════════════════════════
   SCROLL ENTRANCE
   One-shot: each element rises the first time it is seen and is then left
   alone. A thing that re-animates on every crossing turns a page into a
   fidget. Declared under .js-anim, which the head script only sets when the
   reader has not asked for reduced motion.
   ═══════════════════════════════════════════════════════════════════════ */
.js-anim .rise {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 620ms cubic-bezier(0.22, 1, 0.36, 1),
                transform 620ms cubic-bezier(0.22, 1, 0.36, 1);
}

.js-anim .rise.in {
    opacity: 1;
    transform: none;
}

.js-anim .rise.d1 { transition-delay: 60ms; }
.js-anim .rise.d2 { transition-delay: 130ms; }

/* ═══════════════════════════════════════════════════════════════════════
   LEGAL PAGES
   ═══════════════════════════════════════════════════════════════════════ */
.legal-banner {
    background: var(--color-error-bg);
    border-bottom: 1px solid var(--rail);
    color: var(--color-error-fg);
}

.legal-banner div {
    width: min(100% - var(--inset) * 2, var(--frame));
    margin: 0 auto;
    padding: 11px var(--gutter);
    font-size: 13.5px;
    font-weight: 500;
}

/* Three columns: a flexible spacer, the content column, the TOC. The
   spacer is what shifts the reading column toward the centre of the frame
   instead of hugging the left rail, and it is also what gives the TOC its
   own distance from the text rather than sitting right against it. The
   TOC is a sibling of .legal, not a child of it, so none of the article's
   own type rules (underlines, list indents) leak into it. */
.legal-layout {
    display: grid;
    grid-template-columns: minmax(16px, 1fr) minmax(0, 76ch) 190px;
    gap: clamp(40px, 6vw, 72px);
    align-items: start;
    padding: clamp(44px, 6vw, 76px) var(--gutter) clamp(56px, 8vw, 92px);
}

.legal {
    grid-column: 2;
    min-width: 0;
}

/* Sticky, minimal, and read-only until clicked. There are two of these
   (one per language, see impressum.html/privacy.html/terms.html) and both
   are pinned to the same cell: only one is ever in the rendered tree at a
   time (the dual-language display:none rule removes the other one
   entirely), so there is nothing to actually overlap.

   The rule down the left is ONE continuous line (::before, spanning the
   nav's full height) rather than a border per entry, so scrolling never
   reads as a row of disconnected dashes. A single pill (.legal-toc-
   indicator) slides along it to the active entry's position — site.js
   measures and moves it — which is what makes the transition between
   sections a glide instead of a jump cut. */
.legal-toc {
    grid-column: 3;
    grid-row: 1;
    position: sticky;
    top: calc(var(--nav-h) + 40px);
    display: flex;
    flex-direction: column;
    font-size: 12.5px;
    padding-left: 15px;
}

.legal-toc::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 2px;
    background: var(--border-default);
    border-radius: 1px;
}

.legal-toc-indicator {
    position: absolute;
    left: 0;
    width: 2px;
    background: var(--primary);
    border-radius: 1px;
    opacity: 0;
    transition: top 260ms cubic-bezier(0.22, 1, 0.36, 1), height 260ms cubic-bezier(0.22, 1, 0.36, 1),
        opacity 150ms;
}

.legal-toc-indicator.on {
    opacity: 1;
}

.legal-toc a {
    padding: 6px 0;
    color: var(--ink-quaternary);
    text-decoration: none;
    line-height: 1.35;
    transition: color 150ms;
}

.legal-toc a:hover {
    color: var(--ink-primary);
}

.legal-toc a.is-active {
    color: var(--primary-ink);
    font-weight: 500;
}

.legal .h1 {
    font-size: clamp(2rem, 4.4vw, 2.75rem);
    line-height: 1.06;
    margin-bottom: 8px;
}

/* Descendant, not direct child: the legal pages wrap each language in its own
   <section lang>, so the text sits one level deeper than it used to. */
.legal p,
.legal ul,
.legal address {
    color: var(--ink-tertiary);
    font-size: 15.5px;
    margin: 0 0 15px;
}

/* The German and English halves are mutually exclusive now (the nav toggle
   picks one), not stacked, so there is no seam between them to rule off. */
.legal section + section {
    margin-top: 0;
}

.legal address {
    font-style: normal;
}

/* No rule above each heading: the chapters read as one continuous text,
   and the heading's own weight and spacing are what separate them. */
.legal h2 {
    font-size: 19px;
    font-weight: 560;
    letter-spacing: -0.015em;
    margin: 34px 0 9px;
    color: var(--ink-primary);
}

.legal h2:first-of-type {
    margin-top: 0;
}

.legal h3 {
    font-size: 16px;
    font-weight: 560;
    margin: 20px 0 6px;
    color: var(--ink-primary);
}

.legal ul {
    padding-left: 20px;
}

.legal li {
    margin-bottom: 6px;
}

.legal a {
    color: var(--primary-ink);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.legal code {
    font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
    font-size: 13.5px;
    background: var(--surface-subtle);
    border: 1px solid var(--border-default);
    border-radius: 4px;
    padding: 1px 5px;
    color: var(--ink-secondary);
}

.legal .updated {
    font-size: 13.5px;
    color: var(--ink-quaternary);
    margin-bottom: 28px;
}

/* ═══════════════════════════════════════════════════════════════════════
   RESPONSIVE
   -----------------------------------------------------------------------
   Two breaks. At 1000px the text-and-evidence pair stacks, because below
   that the text column is too narrow to hold a 42px heading. At 640px the
   three audience cards become one column and the gutter halves.
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 1240px) {
    :root {
        --inset: 40px;
    }
}

@media (max-width: 1100px) {
    /* Three section links plus a CTA stop fitting next to the wordmark long
       before the page is a phone. They, the theme switch and the language
       switch all move into the burger menu instead — a convenience, not
       the only way to reach any of them (the sections are still one scroll
       away, the two switches still work from whichever page you land on
       next), so they collapse into one control rather than wrapping. */
    .nav-links,
    .nav-right > .theme-btn,
    .nav-right > .lang-switch {
        display: none;
    }

    .nav-burger {
        display: inline-flex;
    }
}

@media (max-width: 1000px) {
    .story,
    .faq {
        grid-template-columns: 1fr;
    }

    .faq {
        gap: 36px;
    }

    /* Stacked, a chapter is as tall as its content and the panel spans the
       full width: the equal-height rule only means anything side by side. */
    .story {
        min-height: 0;
    }

    .story-text {
        max-width: 58ch;
    }

    /* The evidence comes after the claim on a narrow screen, always: a reader
       scrolling a phone should meet the sentence before the picture. */
    .story-media {
        order: 2;
        aspect-ratio: 16 / 11;
    }

    .faq-side {
        position: static;
    }

    .faq-a {
        padding-right: 40px;
    }

    /* No room for a side rail once the content column itself is this
       narrow; the sections are still reachable by scrolling, just not by
       jumping. Back to a single column, so .legal's explicit grid-column: 2
       is reset too — otherwise it would still reserve a now-nonexistent
       first column and sit in an orphaned second one. */
    .legal-layout {
        grid-template-columns: 1fr;
    }

    .legal {
        grid-column: 1;
    }

    .legal-toc {
        display: none;
    }
}

/* Three audience cards need about 300px each before the headings start
   breaking into three lines. Below that they stack rather than squeeze. */
@media (max-width: 900px) {
    .aud-grid {
        grid-template-columns: 1fr;
    }

    .aud-card + .aud-card {
        border-left: 0;
        border-top: 1px solid var(--rail);
    }
}

@media (max-width: 640px) {
    :root {
        --inset: 0px;
        --gutter: 20px;
        --nav-h: 60px;
    }

    .rails {
        display: none;      /* at this width the frame IS the viewport */
    }

    .nav-inner,
    .footer-inner {
        border-left: 0;
        border-right: 0;
    }

    .brand img {
        height: 28px;
    }

    /* The 16px desktop default reads as noticeably large once the column
       it sits in is the full, narrow viewport rather than a ~46ch measure
       next to a panel. 14px on a phone instead; the footer already sets
       its own smaller size independently and is left exactly as it is.
       Elements with their own explicit size (the headings, .lede, the
       legal TOC, etc.) are untouched, since those never inherited this
       value to begin with. */
    body {
        font-size: 14px;
    }

    .nav-right {
        gap: 8px;
    }

    .theme-btn {
        width: 33px;
        height: 33px;
    }

    .lang-switch {
        height: 33px;
    }

    .nav-burger {
        width: 33px;
        height: 33px;
    }

    /* Boxes shrink on a phone, the 14px label does not. */
    .btn {
        height: 34px;
        padding: 0 14px;
    }

    .btn-lg {
        height: 42px;
        padding: 0 20px;
    }

    /* On a phone the hero is the whole first screen: only the twinkle field,
       nothing of Problem peeking up from below. Same formula as the desktop
       rule, inherited rather than capped, now that --nav-h above has its own
       mobile value. */
    .hero-inner {
        min-height: calc(100dvh - var(--nav-h) - 2px);
        gap: 20px;
    }

    /* Taller hero, more sky to fill: the field covers more of the now
       available vertical space (was 75%, matching desktop). The mask's
       fade-in threshold moves out proportionally so the field still comes
       alive at the same relative height within its own box. */
    .hero-field {
        height: 86%;
    }

    .hero-field canvas {
        -webkit-mask-image: linear-gradient(to top, #000 0%, #000 33%, transparent 100%);
        mask-image: linear-gradient(to top, #000 0%, #000 33%, transparent 100%);
    }

    .hero-inner .h1 {
        max-width: 100%;
    }

    .lede {
        max-width: 100%;
        font-size: 1.125rem;
    }

    .story-media {
        aspect-ratio: 4 / 3;
    }

    /* The phone is tall (1410:2896) and was being forced into the same
       wide 4:3 box as the landscape photo and the dashboard shot above,
       so .story-media's own overflow:hidden (the panel is what crops its
       contents, by design) was cropping it top and bottom. The shell's
       own clamp(336px, 35.2vw, 480px) floors at 336px for every width in
       this query (35.2vw only overtakes that floor well past tablet), so
       a fixed, generous height here is what actually fits it, not a ratio. */
    #solution .story-media {
        aspect-ratio: auto;
        min-height: 440px;
    }

    .faq-q {
        gap: 16px;
        padding: 17px 0;
    }

    .faq-a {
        padding-right: 0;
    }

    .faq-plus {
        width: 20px;
        height: 20px;
    }
    .faq-plus::before { top: 9px; }
    .faq-plus::after { left: 9px; }

    .footer-top {
        grid-template-columns: 1fr;
        gap: 34px;
    }

    .two-up {
        grid-template-columns: 1fr;
    }

    /* First/last name is the one pair kept side by side even on a phone
       (the other .two-up, e-mail/position, still stacks) — two short
       inputs read fine at half width and it saves a whole row. */
    .two-up.two-up-names {
        grid-template-columns: 1fr 1fr;
        gap: 10px;
    }

    .modal {
        width: 100%;
        margin: auto auto 0;
        border-radius: var(--radius-pop) var(--radius-pop) 0 0;
        max-height: 92dvh;
    }

    /* The success screen is much shorter than the form it replaces, and it
       is not a sheet, so it gets the bottom-sheet treatment above undone:
       dead centre, fully rounded, and capped close to .done's own 420px
       measure instead of full width, so it reads as a compact card rather
       than a short wide strip stranded at the bottom edge. */
    .modal.modal-request.is-done {
        width: min(100%, 420px);
        margin: auto;
        border-radius: var(--radius-pop);
    }

    /* The bottom-sheet treatment above is right for the contact form (it is
       the one modal with a scrollable form inside it), but the zoom
       lightbox is a picture, not a form, and pinning it to the bottom
       edge left most of the viewport above it empty. Centred instead, same
       as it already is on every wider screen. */
    .modal.zoom-modal {
        margin: auto;
        border-radius: var(--radius-pop);
        max-height: none;
    }

    /* No spare width to push the button into on a phone this narrow — back
       to the plain inset rather than risk it clipping off the viewport
       edge. */
    .zoom-modal--phone .modal-close {
        top: 14px;
        right: 14px;
    }

    .request-demo {
        grid-template-columns: 1fr;
    }

    /* Condensed intro: the brand block goes away on a phone and the modal
       opens straight onto the title and the pitch, so the whole form fits
       without scrolling to reach the submit button. The aside's gradient
       and the h2/p copy stay — only the logo row and its padding go. */
    .request-aside-brand {
        display: none;
    }

    /* A little taller than the content alone needs, so more of the brand
       gradient shows above the form: +8px top, +20px bottom over the old
       18/14, +28px total. */
    .request-aside {
        padding: 26px 20px 34px;
        gap: 6px;
        border-radius: var(--radius-pop) var(--radius-pop) 0 0;
    }

    .request-aside .h2 {
        font-size: 1.25rem;
    }

    .request-form {
        padding: 16px 20px 18px;
        gap: 12px;
    }

    /* The base rule above dropped the scroll (desktop's side-by-side
       content fits without it); a phone's stacked column is taller, so
       this breakpoint keeps its own scroll cap as a safety net. */
    .modal-scroll {
        max-height: 92dvh;
        overflow-y: auto;
    }

    /* THE SHEET. On a phone the contact modal arrives from the bottom edge
       and leaves the same way, like an iOS action sheet, instead of the
       desktop's small fade-and-lift. The open animation replaces the
       desktop one for this breakpoint; the close is a class that
       contact.js adds just before dialog.close() (a <dialog> cannot
       animate its own removal), removed again on the next open. */
    .modal.modal-request[open] {
        animation: sheet-in 340ms cubic-bezier(0.22, 1, 0.36, 1);
    }

    .modal.modal-request.is-closing {
        animation: sheet-out 240ms cubic-bezier(0.4, 0, 1, 1) forwards;
    }

    @keyframes sheet-in {
        from { transform: translateY(100%); }
        to   { transform: none; }
    }

    @keyframes sheet-out {
        from { transform: none; }
        to   { transform: translateY(100%); }
    }

    /* Audience icons come down a step on a phone: 67px tiles eat a card
       that is already sharing its row with the title. 56px keeps the tile
       readable and hands the width back to the copy. */
    .aud-card .aud-icon {
        width: 56px;
        height: 56px;
    }

    .aud-card .aud-icon svg {
        width: 28px;
        height: 28px;
    }

    /* The all-caps violet eyebrows read a size too loud on a phone. */
    .eyebrow {
        font-size: 13px;
    }

    /* FAQ questions: 18px is headline weight in a single column. */
    .faq-q {
        font-size: 1rem;
    }

    /* Icon beside its title rather than above it, so a thumb moving down
       the three cards reads "icon, name" as one line instead of the icon
       eating a full row of its own before the heading arrives. Grid, not
       flex-direction:row, because the paragraph still needs to span full
       width underneath both — named areas place each of the three existing
       children without touching the markup. */
    .aud-card {
        display: grid;
        grid-template-columns: auto 1fr;
        grid-template-areas:
            'icon title'
            'copy copy';
        align-items: center;
        column-gap: 16px;
        row-gap: 10px;
    }

    .aud-card .aud-icon {
        grid-area: icon;
        margin-bottom: 0;
    }

    .aud-card .h3 {
        grid-area: title;
    }

    .aud-card .copy {
        grid-area: copy;
    }
}

/* ═══════════════════════════════════════════════════════════════════════
   REDUCED MOTION
   -----------------------------------------------------------------------
   Every animation on this page has a STATIC fallback, not just a disabled
   one: the twinkle fields become a painted mosaic of the same pixels at the
   same gradient, and everything that rises is simply already in place.
   ═══════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }

    .js-anim .rise {
        opacity: 1;
        transform: none;
    }

    .hero-field canvas,
    .cta-field canvas {
        display: none;
    }

    /* The painted stand-in. A flat violet, masked into a 6px grid of 5px
       squares (exactly TwinkleField's PIXEL and SQUARE) and then faded out
       toward the top by the same kind of vertical gradient. The plain
       background declared first is what a browser without mask-composite
       falls back to, which is a soft wash rather than a blank box. */
    .hero-field::after,
    .cta-field::after {
        content: '';
        position: absolute;
        inset: 0;
        background: linear-gradient(
            to top,
            color-mix(in srgb, var(--primary-select) 55%, transparent),
            transparent 70%
        );
    }

    @supports (mask-composite: intersect) or (-webkit-mask-composite: source-in) {
        .hero-field::after,
        .cta-field::after {
            background: var(--primary-select);
            -webkit-mask-image:
                repeating-linear-gradient(to right, #000 0 5px, transparent 5px 6px),
                repeating-linear-gradient(to bottom, #000 0 5px, transparent 5px 6px),
                linear-gradient(to top, #000 0%, transparent 78%);
            mask-image:
                repeating-linear-gradient(to right, #000 0 5px, transparent 5px 6px),
                repeating-linear-gradient(to bottom, #000 0 5px, transparent 5px 6px),
                linear-gradient(to top, #000 0%, transparent 78%);
            -webkit-mask-composite: source-in, source-in;
            mask-composite: intersect, intersect, add;
        }
    }
}
