/* ==========================================================================
   Ceyu — marketing site stylesheet
   One file, no framework, no build step.

   Art style: drawn / comic — white mode. The Webflow layout, spacing and
   information architecture stay exactly as they are; only the surface
   language changes. Three rules carry the whole look:
     1. Every self-contained object is outlined in ink (2px --stroke).
        Dividers *inside* an object stay 1px and light (--line). That
        distinction is what keeps a table from collapsing into a grid of
        separate boxes.
     2. Elevation is a hard ink offset, never a blur. Hover lifts by the
        offset, the press drops into it — the mechanical click is what
        this style uses instead of an elevation animation, so the offsets
        and the translate values have to stay in sync.
     3. Colour is flat. Violet carries the brand and the primary button;
        the pastel --tile-* fills carry the playfulness. No gradients.

   Two accents, and the distinction matters: --accent is the brand violet
   for fills (white text on it clears AA at 4.9:1); --accent-strong is the
   same hue darkened to 7.1:1 and is the only one allowed to carry text.

   The violet matches the Creem checkout branding, so the purchase flow
   does not change colour halfway through.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Ground and surfaces. White ground with a faint squared texture; cards
     are white too, and the ink outline is what separates them. */
  --ground: #ffffff;
  --surface: #f7f5fb; /* inset panels, tinted a step toward the brand hue */
  --surface-2: #ffffff; /* cards — white, so the outline does the work */
  --ink: #151617; /* the Creem ink, near-black but not pure */

  /* Accent — the brand violet, matched to the Creem checkout.
     --accent carries fills and the primary button (white on it is 4.9:1).
     --accent-strong carries text and focus rings (7.1:1 on white).
     Never swap them. */
  --accent: #8d4edc;
  --accent-hover: #7b3ecb;
  --accent-strong: #6b2fb8;
  --accent-strong-hover: #5a239e;
  --on-accent: #ffffff;

  /* Ink tints — measured on white: --ink-70 is 9.4:1, --ink-60 is 5.6:1,
     so both clear AA for body text. --ink-40 and below are decoration and
     must never carry text. */
  --ink-90: #151617e6;
  --ink-70: #151617c4;
  /* 0x9d, not 0x99. At 60% the composite fell to 4.39:1 on the coral tile
     and 4.48 on the sky one — under the floor for body text on exactly the
     surfaces this colour is used on. 0x9d clears 4.6:1 on every tint. */
  --ink-60: #1516179d;
  --ink-50: #15161799;
  --ink-40: #15161770;
  --ink-20: #1516172e;
  --ink-10: #1516171a;
  --ink-06: #1516170f;

  /* Accent tints */
  --accent-40: #8d4edc66;
  --accent-30: #8d4edc4d;
  --accent-20: #8d4edc33;
  --accent-10: #8d4edc1a;
  --accent-06: #f6f1fd;

  /* Flat comic pops — small marks, chart series and status states. */
  --pop-purple: #8d4edc;
  --pop-coral: #ff7a59;
  --pop-mint: #21c07f;
  --pop-sun: #ffc531;
  --pop-sky: #4cb8ff;
  --pop-pink: #ff6fae;

  /* Pastel tiles — flat fills behind icons and inside illustrations. */
  --tile-purple: #efe6ff;
  --tile-coral: #ffe2d6;
  --tile-mint: #d9f7e8;
  --tile-sun: #fff2c9;
  --tile-sky: #ddf1ff;
  --tile-pink: #ffe1ef;

  /* A single warm secondary, used only inside the product illustrations. */
  --warm: #ffe6a8;

  /* The footer closes the page in the brand violet itself — deep enough
     that white text on it clears AA comfortably (8.2:1) rather than
     scraping past it. Measured, not guessed: at the lighter violet tried
     first, the blurb and the bottom row came out at 4.0 and 3.5 to one,
     under the 4.5 floor for body text. The wave above uses the same value, so the two can
     never drift apart. --footer-ink is the pale violet that marks a link
     hover down there; plain white would leave nothing between the resting
     and the hovered state. */
  /* The footer bar and the header bar are the same surface at opposite ends
     of the page, so they carry one value. Anything else reads as two brands. */
  --footer: var(--accent);
  --footer-ink: #ffffff;

  /* Borders — see rule 1. --line is the light divider inside an object,
     --stroke is the ink outline around one. */
  --line: #e7e4ee;
  --line-strong: var(--ink);
  --stroke-w: 2px;
  --stroke: var(--stroke-w) solid var(--ink);

  /* Type — Figtree: round and friendly, which is what a drawn style
     needs. Loud uppercase labels stay from the Webflow build. */
  --font: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, "Noto Sans", "Noto Sans JP", "Noto Sans KR",
    "Noto Sans SC", "Noto Sans Arabic", "Noto Sans Devanagari",
    "Noto Sans Thai", sans-serif;
  --mono: var(--font);

  /* The display face, for the top heading on a page and nothing else.
     Gasoek One ships one weight, so anything using it must ask for 400 —
     a heavier request makes the browser smear it synthetically, and at
     this size that is instantly visible. It is already so wide and heavy
     that the tracking has to open back up rather than tighten. */
  --font-display: "Gasoek One", "Figtree", -apple-system, BlinkMacSystemFont,
    "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Fluid type scale. The hero runs to 7rem — larger than the 6rem the
     house rule allows, and deliberate: the homepage headline is the one
     place this brand shouts, and it is the only element on that fold. It
     stays at -0.04em tracking, the floor below which letters touch. */
  --t-h0: clamp(2.75rem, 0.6rem + 7.4vw, 7rem);
  --t-h1: clamp(2.25rem, 1.4rem + 3.4vw, 3.5rem);
  --t-h2: clamp(2rem, 1.4rem + 2.2vw, 3.5rem);
  --t-h3: clamp(1.35rem, 1.1rem + 1vw, 1.78rem);
  --t-h4: clamp(1.125rem, 1rem + 0.5vw, 1.5rem);
  --t-lg: 1.125rem;
  --t-base: 1rem;
  --t-sm: 0.9375rem;
  --t-xs: 0.8125rem;
  --t-label: 0.8125rem;

  /* Aliases used by the generated calculator pages. */
  --fs-xs: var(--t-xs);
  --fs-sm: var(--t-sm);
  --fs-md: var(--t-base);
  --fs-lg: var(--t-lg);
  --fs-xl: 1.5rem;

  /* Spacing — a 4px-derived scale */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.25rem;
  --s-6: 1.5rem;
  --s-7: 1.75rem;
  --s-8: 2rem;
  --s-10: 2.5rem;
  --s-12: 3rem;
  --s-16: 4rem;
  --s-20: 5rem;
  --s-24: 6rem;
  --s-32: 8rem;

  /* Radii — one step rounder than the Webflow build; rounder reads as
     drawn rather than machined. */
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 22px;
  --r-round: 100px;

  /* How far the "most popular" badge hangs above its card.

     Geometrically, half the badge height puts its midpoint exactly on the
     card's top border — and measured, that is what 26px did, to within a
     pixel. It still read as sitting low, because the half below the line
     lands on saturated violet while the half above it lands on white, and
     the eye weighs the dark half heavier. So the badge is lifted past the
     geometric centre until it looks centred, which is the only centre that
     matters here.

     Read by the badge and by the scrolling plan row that has to reserve
     headroom for it, so the two cannot drift apart when the badge grows. */
  --plan-badge-rise: 28px;

  /* Layout — Webflow container is 1400px wide with tight gutters. */
  --container: 1400px;
  --container-lg: 1400px;
  --container-sm: 1000px;
  --gutter: clamp(1rem, 4vw, 1.5rem);
  --header-h: 68px;

  /* Section rhythm */
  --section-y: clamp(3.75rem, 9vw, 6.25rem);

  /* Motion */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --fast: 140ms;
  --base: 260ms;
  --slow: 600ms;

  /* Elevation — a hard ink offset, no blur (rule 2). The offset is the
     only height statement available once the blur is gone, and --press is
     what a pressed object translates by to land in its own shadow. */
  --shadow-1: 2px 2px 0 var(--ink);
  --shadow-2: 4px 4px 0 var(--ink);
  --shadow-3: 6px 6px 0 var(--ink);
  --shadow-accent: 4px 4px 0 var(--ink);
  --press: 2px;

  color-scheme: light;
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */

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

* {
  margin: 0;
}

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

/* Plain white. The squared texture appears only where it carries meaning —
   behind the hero and inside the closing panel. Running it under every
   page turned a motif into wallpaper, and wallpaper is the first thing
   that makes a page look busy rather than considered. */
body {
  background-color: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-base);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

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

/* The browser's own `[hidden] { display: none }` is a bare attribute rule,
   so any class here that sets display — .plan__features, .faq, .steps —
   silently wins and shows an element JavaScript just hid. Every toggle on
   this site uses the hidden attribute, so it has to outrank them. */
[hidden] {
  display: none !important;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  cursor: pointer;
}

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--fast) var(--ease);
}

ul,
ol {
  padding: 0;
  list-style: none;
}

table {
  border-collapse: collapse;
  width: 100%;
}

:where(h1, h2, h3, h4, h5, h6) {
  font-weight: 800;
  line-height: 1.12;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

/* Focus: one consistent ring everywhere, only for keyboard users. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

::selection {
  background: rgba(13, 153, 255, 0.22);
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The generated company pages (newsroom, integrations, calculators) were
   written against a `.wrap` class; aliasing keeps them on the grid. */
.wrap {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.wrap--narrow {
  max-width: var(--container-sm);
}

.container--lg {
  max-width: var(--container-lg);
}

.container--sm {
  max-width: var(--container-sm);
}

.section {
  padding-block: var(--section-y);
  position: relative;
}

.section--tight {
  padding-block: clamp(2.5rem, 6vw, 5rem);
}

/* A hairline that stops short of the viewport edge. */
.section--ruled::before {
  content: "";
  position: absolute;
  inset-inline: var(--gutter);
  top: 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    var(--line) 12%,
    var(--line) 88%,
    transparent
  );
}

.stack > * + * {
  margin-top: var(--s-4);
}

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

/* Available to assistive technology, absent from the visual layout. */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--s-4);
  top: var(--s-4);
  z-index: 200;
  padding: var(--s-3) var(--s-5);
  background: var(--ink);
  color: #ffffff;
  border-radius: var(--r-sm);
  font-weight: 800;
  font-size: var(--t-sm);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  transform: translateY(-200%);
  transition: transform var(--fast) var(--ease);
}

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

/* --------------------------------------------------------------------------
   4. Hero backdrop
   The Webflow build sits on the plain canvas colour — no bloom, no grid.
   The element stays for markup compatibility and renders nothing.
   -------------------------------------------------------------------------- */

.canvas-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

/* The hero sits on plain white. It used to carry squared paper here, but
   the motif already does its work inside the product frames and in the
   closing panel; behind the headline it only competed with the wave above
   it. The element itself stays — nine pages mark it up — and renders
   nothing. */

/* --------------------------------------------------------------------------
   4b. Signature strokes
   Same class names as before; visuals ported to the Webflow language.
   -------------------------------------------------------------------------- */

.drawn-underline {
  position: relative;
}

.drawn-underline::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.28em;
  height: 2px;
  border-radius: var(--r-round);
  background: var(--ink-10);
  transition: background var(--fast) var(--ease);
  pointer-events: none;
}

.drawn-underline:hover::after,
.drawn-underline:focus-visible::after {
  background: var(--accent);
}

.rule-drawn {
  height: 1px;
  border: 0;
  margin: 0;
  background: linear-gradient(
    90deg,
    transparent,
    var(--line) 10%,
    var(--line) 90%,
    transparent
  );
}

.tag--sticker {
  rotate: 0deg;
  box-shadow: var(--shadow-1);
}

.tag--sticker:nth-of-type(even) {
  rotate: 0deg;
}

.drawn-ring {
  position: relative;
}

.drawn-ring::before {
  content: none;
}

/* --------------------------------------------------------------------------
   Download page
   -------------------------------------------------------------------------- */

.dl-head {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  margin-bottom: var(--s-8);
}

.dl-version {
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

/* Nothing published yet — stated as a visible notice, not quiet grey text. */
.dl-state--none {
  max-width: 68ch;
  padding: var(--s-4) var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--tile-sun);
  box-shadow: var(--shadow-1);
  color: var(--ink-90);
}

.dl-card__title {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.dl-card__icon {
  width: 1.35em;
  height: 1.35em;
  flex: none;
  color: var(--ink);
}

.dl-card[data-unavailable] .dl-card__icon {
  color: var(--ink-50);
}

.dl-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.dl-card__foot {
  margin-top: auto;
  padding-top: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  align-items: flex-start;
}

.dl-card__action {
  width: 100%;
}

.dl-card__meta {
  font-size: var(--t-xs);
  color: var(--ink-60);
  overflow-wrap: anywhere;
}

.dl-card__pending {
  font-size: var(--t-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--ink-60);
}

/* White for all platform cards, the visitor's own gets the black ring —
   the same highlight the featured pricing card uses. */
.features-grid.dl-grid > .card:nth-child(n) {
  background: var(--surface-2);
}

.dl-card[data-unavailable] {
  background: var(--surface);
}

.features-grid.dl-grid > .card.is-yours:nth-child(n) {
  order: -1;
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
}

.dl-card__sum {
  font-size: var(--t-xs);
}

.dl-card__sum summary {
  cursor: pointer;
  color: var(--accent-strong);
}

.dl-card__hash {
  margin-top: var(--s-2);
  font-size: 0.6875rem;
  overflow-wrap: anywhere;
  color: var(--ink-70);
}

/* --------------------------------------------------------------------------
   Horizontal tracks (workflow steps) — native scroll with snap points.
   -------------------------------------------------------------------------- */

.steps {
  display: flex;
  gap: var(--s-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  padding-block-end: var(--s-4);
  margin-inline: calc(-1 * var(--gutter));
  padding-inline: var(--gutter);
  scrollbar-width: thin;
}

.steps > .card {
  flex: 0 0 clamp(240px, 27vw, 320px);
  scroll-snap-align: start;
}

.steps__nav {
  display: none;
  gap: var(--s-2);
  margin-top: var(--s-4);
}

@media (hover: hover) and (pointer: fine) {
  .steps__nav {
    display: flex;
  }
}

.steps__btn {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  color: var(--ink);
  transition: box-shadow var(--fast) var(--ease),
    transform var(--fast) var(--ease), border-color var(--fast) var(--ease),
    color var(--fast) var(--ease);
}

.steps__btn:hover:not(:disabled) {
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.steps__btn:active:not(:disabled) {
  box-shadow: none;
  translate: var(--press) var(--press);
}

.steps__btn:disabled {
  opacity: 0.35;
  cursor: default;
  box-shadow: none;
}

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

  .steps__btn:hover:not(:disabled),
  .steps__btn:active:not(:disabled) {
    translate: 0 0;
  }
}

/* --------------------------------------------------------------------------
   Status plug — the plug-and-socket state icon on /status.
   -------------------------------------------------------------------------- */

/* The drawing fills the disc edge to edge so the two leads can run off it. The
   disc does the cutting, which is why it clips: a cable that stops in mid-air
   inside the circle reads as an unfinished drawing. */
.status-summary__icon {
  overflow: hidden;
}

.plug {
  display: block;
  width: 100%;
  height: 100%;
}

/* Apart is the resting state: the icon should not claim a connection before
   one has been measured. Both halves move, so the drawing stays centred in
   the disc whether it is joined or pulled apart — and six units each opens a
   real gap with the pins standing bare in it, not a nudge.

   The translate runs along the rotated axis, because these sit inside the
   group that carries the rotation. */
.plug__body,
.plug__socket {
  transition: translate var(--base) var(--ease);
}

.status-summary[data-state="operational"] .plug__body {
  translate: -8.5px 0;
}

.status-summary[data-state="operational"] .plug__socket {
  translate: 8.5px 0;
}

/* Halfway, and still visibly not connected. */
.status-summary[data-state="loading"] .plug__body {
  translate: -3px 0;
}

.status-summary[data-state="loading"] .plug__socket {
  translate: 3px 0;
}

/* The sparks are what the separation throws off, so they have nothing left to
   say once there is no separation.

   The lead used to carry two ink pills for its holes. At this size, inside an
   outlined shape that is itself rotated, they did not read as openings in an
   end face — they read as two dots lying on top of the body. The open end is
   already drawn: it is the wide edge of the taper. */
.plug__sparks {
  transition: opacity var(--fast) var(--ease);
}

.status-summary[data-state="operational"] .plug__sparks {
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes plug-wait {
    0%, 100% { translate: -3px 0; }
    50% { translate: -5.5px 0; }
  }

  @keyframes plug-wait-lead {
    0%, 100% { translate: 3px 0; }
    50% { translate: 5.5px 0; }
  }

  .status-summary[data-state="loading"] .plug__body {
    animation: plug-wait 1.7s var(--ease) infinite;
  }

  .status-summary[data-state="loading"] .plug__socket {
    animation: plug-wait-lead 1.7s var(--ease) infinite;
  }
}

@media (prefers-reduced-motion: reduce) {
  .plug__body,
  .plug__socket,
  .plug__sparks {
    transition-duration: 0.01ms;
  }
}

/* --------------------------------------------------------------------------
   Newsroom bird — decorative, aria-hidden, chirps on hover/tap.
   -------------------------------------------------------------------------- */

.bird {
  width: clamp(64px, 8vw, 96px);
  height: auto;
  overflow: visible;
  cursor: pointer;
}

.bird__note {
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes bird-bob {
    0%, 100% { translate: 0 0; }
    50% { translate: 0 -1px; }
  }

  @keyframes bird-tip {
    0%, 100% { rotate: 0deg; }
    35% { rotate: -13deg; }
    70% { rotate: 4deg; }
  }

  @keyframes bird-beak {
    0%, 100% { rotate: 0deg; }
    30%, 60% { rotate: 17deg; }
  }

  @keyframes bird-note {
    0% { opacity: 0; translate: 0 0; scale: 0.6; }
    25% { opacity: 1; }
    100% { opacity: 0; translate: 14px -18px; scale: 1; }
  }

  .bird__body {
    animation: bird-bob 4.6s var(--ease) infinite;
  }

  .bird__head {
    transform-origin: 46px 34px;
  }

  .bird__beak {
    transform-origin: 60px 33px;
  }

  .bird:hover .bird__head,
  .bird.is-chirping .bird__head {
    animation: bird-tip 0.7s var(--ease);
  }

  .bird:hover .bird__beak,
  .bird.is-chirping .bird__beak {
    animation: bird-beak 0.7s var(--ease);
  }

  .bird:hover .bird__note,
  .bird.is-chirping .bird__note {
    animation: bird-note 0.9s var(--ease) forwards;
  }

  .bird:hover .bird__note--2,
  .bird.is-chirping .bird__note--2 {
    animation-delay: 0.18s;
  }
}

/* --------------------------------------------------------------------------
   Homepage sections
   -------------------------------------------------------------------------- */

/* Four answers to "what is this for" directly under the hero. */
.caps__grid {
  display: grid;
  gap: var(--s-8) var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
}

.cap {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding-top: var(--s-5);
  border-top: 1px solid var(--line);
}

.cap__role {
  font-size: var(--t-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--ink-60);
}

/* ------------------------------------------------------------ Product tour
   Styled after the Webflow billing selector: a grey segmented control whose
   active option is a white pill with a small shadow. */

.tour__tabs {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: var(--s-6);
  padding: 4px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.tour__tab {
  padding: 12px 24px;
  border: 0;
  border-radius: var(--r-round);
  background: transparent;
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--ink-60);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease);
}

.tour__tab:hover {
  color: var(--ink);
}

.tour__tab[aria-selected="true"] {
  background: var(--ink);
  color: var(--ground);
}

.tour__stage {
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--shadow-2);
  padding: clamp(var(--s-4), 3vw, var(--s-8));
}

.tour__art {
  width: 100%;
  height: auto;
  display: block;
}

.tour__caption {
  margin-top: var(--s-5);
  max-width: 62ch;
  color: var(--ink-70);
  font-weight: 500;
}

/* The inline illustrations were drawn against thick ink outlines. Scoping
   --ink to a soft grey and thinning the strokes turns them into keyline
   drawings without touching a single SVG. */
.tour__art,
.ph__art {
  --ink: #151617;
}

.tour__art g,
.tour__art rect,
.tour__art path,
.tour__art circle,
.ph__art g,
.ph__art rect,
.ph__art path,
.ph__art circle {
  stroke-width: 2;
}

/* ------------------------------------------------------- Problem / solution */

.vs__grid {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}

.vs__col {
  padding: clamp(var(--s-5), 3vw, var(--s-8));
  border: var(--stroke);
  border-radius: var(--r-xl);
}

.vs__col--bad {
  background: var(--surface);
  border-color: var(--ink-20);
  color: var(--ink-70);
}

/* The "good" column gets the featured treatment: white card, black ring —
   the same highlight the PRO pricing card carries. */
.vs__col--good {
  background: var(--tile-mint);
  box-shadow: var(--shadow-2);
}

.vs__list {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-5);
}

.vs__list li {
  position: relative;
  padding-inline-start: var(--s-8);
  line-height: 1.5;
  font-weight: 500;
}

.vs__col--bad .vs__list li::before,
.vs__col--good .vs__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.35em;
  width: 14px;
  height: 14px;
}

.vs__col--bad .vs__list li::before {
  border-top: 2px solid var(--ink-40);
  translate: 0 4px;
}

.vs__col--good .vs__list li::before {
  border-inline-start: 3px solid var(--ink);
  border-bottom: 3px solid var(--ink);
  rotate: -45deg;
  height: 8px;
}

/* ------------------------------------------------------------- Switch cards */

.switch__row {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
}

.switch__card {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-6);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--base) var(--ease),
    background var(--base) var(--ease), translate var(--base) var(--ease);
}

.switch__card:hover {
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
  text-decoration: none;
}

.switch__from {
  font-family: var(--font);
  font-size: var(--t-h4);
  font-weight: 800;
  letter-spacing: -0.02em;
}

.switch__to {
  color: var(--ink-70);
  font-size: var(--t-sm);
  font-weight: 500;
}

@media (prefers-reduced-motion: reduce) {
  .switch__card:hover {
    translate: 0 0;
  }
}

/* --------------------------------------------------------------------------
   Announcement bar — the Webflow banner is Miro-yellow with black text.
   -------------------------------------------------------------------------- */

.promo {
  background: var(--pop-sun);
  color: var(--ink);
  border-bottom: var(--stroke);
  position: relative;
  z-index: 101;
}

.promo[hidden] {
  display: none;
}

.promo__inner {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: 0.65rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  flex-wrap: wrap;
  font-size: 14px;
  font-weight: 700;
}

.promo__text {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
}

.promo__badge {
  padding: 0.15rem var(--s-3);
  border-radius: 4px;
  background: var(--ink);
  color: #ffffff;
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.promo__link {
  font-weight: 800;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-size: 13px;
}

.promo__close {
  margin-inline-start: auto;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-round);
  color: inherit;
  opacity: 0.7;
}

.promo__close:hover {
  opacity: 1;
  background: rgba(0, 0, 0, 0.1);
}

.promo :focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   Placeholders — canvas-grey stand-ins, the Webflow bento placeholder look.
   -------------------------------------------------------------------------- */

.ph {
  margin: 0;
}

.ph__art {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--r-lg);
  background: transparent;
}

.ph__note {
  margin-top: var(--s-3);
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--ink-60);
}

/* Scenes: the full-size drawn illustrations beside the alternating feature
   rows. Same object language as a card — ink outline, hard offset — so a
   row reads as one composition rather than copy next to a picture. */
.scene {
  margin: 0;
  border: var(--stroke);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-2);
  background: var(--surface-2);
}

.scene--sky { background: var(--tile-sky); }
.scene--purple { background: var(--tile-purple); }
.scene--mint { background: var(--tile-mint); }
.scene--sun { background: var(--tile-sun); }
.scene--coral { background: var(--tile-coral); }

.scene__art {
  display: block;
  width: 100%;
  height: auto;
}

/* ------------------------------------------------------- Stage carousel --

   One scene at a time on a lit stage, with the titles running past above
   it. The neighbouring titles are deliberately left legible at the edges:
   a carousel that hides everything but the current slide gives no reason
   to wait for the next one. */

.stage {
  position: relative;
  overflow: hidden;
  border-block: var(--stroke);
  background: var(--accent-06);
  padding-block: clamp(3rem, 7vw, 5rem);
}

/* Two soft washes behind the stage. They sit under everything and never
   touch the outlines — the drawn objects stay flat, the room they are in
   does not have to be. */
.stage__glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(38rem 26rem at 8% 62%, rgb(255 122 89 / 0.16), transparent 62%),
    radial-gradient(42rem 30rem at 94% 44%, rgb(141 78 220 / 0.2), transparent 64%);
}

.stage > *:not(.stage__glow) {
  position: relative;
}

/* ------------------------------------------------------------- Titles -- */

.stage__titles {
  overflow: hidden;
  margin-block: clamp(1rem, 3vw, 2rem) var(--s-5);
  /* The band fades rather than cutting, so a half-title at the edge reads
     as "there is more" instead of as a rendering fault. */
  mask-image: linear-gradient(
    90deg,
    transparent,
    #000 16%,
    #000 84%,
    transparent
  );
}

.stage__titles-track {
  display: flex;
  align-items: center;
  gap: clamp(2.5rem, 6vw, 5rem);
  width: max-content;
  /* Set by app.js: the offset that puts the active title in the middle. */
  translate: var(--stage-shift, 0px) 0;
  transition: translate var(--slow) var(--ease-out);
}

.stage__title {
  flex: 0 0 auto;
  margin: 0;
  font-size: clamp(1.75rem, 1rem + 3.4vw, 3.25rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.05;
  white-space: nowrap;
  color: var(--ink-20);
  transition: color var(--base) var(--ease), scale var(--base) var(--ease-out);
  scale: 0.94;
  transform-origin: center;
}

.stage__title.is-active {
  color: var(--ink);
  scale: 1;
}

/* -------------------------------------------------------------- Lead -- */

.stage__lead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  min-height: 5.5rem;
  text-align: center;
}

.stage__desc {
  max-width: 52ch;
  padding: var(--s-3) var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-70);
}

/* The module label sits where a customer badge would go on a site that
   had customers to name. It says which half of the product this happens
   in, which is true and also more useful. */
.stage__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-1);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

/* ------------------------------------------------------------- Stage -- */

.stage__frame {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.5rem, 2vw, var(--s-8));
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  margin-top: var(--s-6);
}

.stage__arrow {
  flex: none;
  width: 52px;
  height: 52px;
  display: grid;
  place-items: center;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  color: var(--ink);
  transition: background var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease), translate var(--fast) var(--ease);
}

.stage__arrow:hover {
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.stage__arrow:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

/* The slides are stacked in one grid cell, so the frame keeps the height
   of the tallest scene and nothing jumps as they cross-fade. */
.stage__slides {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 880px;
  display: grid;
}

.stage__slide {
  grid-area: 1 / 1;
  margin: 0;
  border: var(--stroke);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-3);
  opacity: 0;
  scale: 0.97;
  translate: 0 10px;
  pointer-events: none;
  transition: opacity var(--base) var(--ease), scale var(--base) var(--ease-out),
    translate var(--base) var(--ease-out);
}

.stage__slide.is-active {
  opacity: 1;
  scale: 1;
  translate: 0 0;
  pointer-events: auto;
}

.stage--sky { background: var(--tile-sky); }
.stage--purple { background: var(--tile-purple); }
.stage--sun { background: var(--tile-sun); }
.stage--mint { background: var(--tile-mint); }
.stage--coral { background: var(--tile-coral); }

.stage__slide .demo__art {
  display: block;
  width: 100%;
  height: auto;
}

/* --------------------------------------------------------------- Nav -- */

/* Empty above 720px, and `gap` on an empty flex row costs nothing. Below
   that width app.js moves the two arrow buttons in here. The pip row and
   the play/pause control that used to live here are gone: the stage no
   longer advances on its own, so there is nothing to pause and no position
   to report that the arrows and the title band do not already show. */
.stage__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
}

.stage__nav:not(:empty) {
  margin-top: var(--s-6);
}

@media (max-width: 720px) {
  /* app.js moves the arrows down into the nav row at this width: either
     side of the stage they would eat a third of the scene between them. */
  .stage__frame {
    padding-inline: var(--s-4);
  }

  .stage__arrow {
    width: 44px;
    height: 44px;
  }

  .stage__desc {
    font-size: var(--t-xs);
  }

  /* The fade has to start much closer to the edge here, or it eats the
     first and last word of the *active* title — the one thing in the band
     that must always be fully legible. The titles shrink to match. */
  .stage__titles {
    mask-image: linear-gradient(
      90deg,
      transparent,
      #000 5%,
      #000 95%,
      transparent
    );
  }

  .stage__title {
    font-size: clamp(1.35rem, 5.6vw, 1.75rem);
  }

  .stage__titles-track {
    gap: var(--s-8);
  }
}

@media (prefers-reduced-motion: reduce) {
  .stage__titles-track,
  .stage__title,
  .stage__slide {
    transition-duration: 0.01ms;
  }

  .stage__arrow:hover,
  .stage__arrow:active {
    translate: 0 0;
  }
}

/* --------------------------------------------------------------- Demos --

   The drawn scenes on the stage. Each plays only while its slide carries
   .is-active, and every animation ends on the finished picture (`both`
   fill), so a slide that has never played still reads as a complete
   illustration rather than an empty frame. */

.demo__art {
  display: block;
  width: 100%;
  height: auto;
}

@media (prefers-reduced-motion: no-preference) {
  /* 1 — a file is dragged in and lands in the marked slot. */
  @keyframes demo-drop {
    0% { translate: 96px -150px; scale: 0.82; opacity: 0; }
    18% { opacity: 1; }
    62% { translate: 0 0; scale: 1; }
    68% { scale: 1.03; }
    100% { translate: 0 0; scale: 1; opacity: 1; }
  }

  @keyframes demo-slot {
    0%, 55% { opacity: 1; }
    75%, 100% { opacity: 0; }
  }

  /* The cursor stays on the board at the end. Fading it out left the
     finished frame showing a picture and nothing else, which reads as an
     empty illustration rather than as "someone just dropped this". */
  @keyframes demo-cursor {
    0% { translate: 430px -40px; }
    60% { translate: 330px 180px; }
    66% { scale: 0.82; }
    72% { scale: 1; }
    100% { translate: 372px 232px; }
  }

  .stage__slide.is-active .demo__drop {
    animation: demo-drop 2.6s var(--ease-out) both;
  }

  .stage__slide.is-active .demo__slot {
    animation: demo-slot 2.6s linear both;
  }

  .stage__slide.is-active .demo__cursor {
    animation: demo-cursor 2.6s var(--ease-out) both;
  }

  /* 2 — the note travels along the arrow and a task row appears. */
  @keyframes demo-note {
    0%, 12% { translate: 0 0; rotate: 0deg; }
    55% { translate: 18px -6px; rotate: -3deg; }
    100% { translate: 0 0; rotate: 0deg; }
  }

  @keyframes demo-draw {
    from { stroke-dasharray: 0 200; }
    to { stroke-dasharray: 200 0; }
  }

  @keyframes demo-task-in {
    0%, 30% { opacity: 0; translate: 22px 0; }
    70%, 100% { opacity: 1; translate: 0 0; }
  }

  @keyframes demo-tick {
    0%, 72% { stroke-dashoffset: 22; }
    100% { stroke-dashoffset: 0; }
  }

  .stage__slide.is-active .demo__note {
    animation: demo-note 2.8s var(--ease) both;
  }

  .stage__slide.is-active .demo__arrow {
    animation: demo-draw 1.1s var(--ease-out) both;
  }

  .stage__slide.is-active .demo__task {
    animation: demo-task-in 2.4s var(--ease-out) both;
  }

  .stage__slide.is-active .demo__tick {
    stroke-dasharray: 22;
    animation: demo-tick 2.4s var(--ease-out) both;
  }

  /* 3 — the first bar slips and the dependent bars follow it. */
  @keyframes demo-shift {
    0%, 20% { translate: 0 0; }
    70%, 100% { translate: 62px 0; }
  }

  @keyframes demo-links-fade {
    0%, 25% { opacity: 1; }
    45% { opacity: 0.35; }
    75%, 100% { opacity: 1; }
  }

  .stage__slide.is-active .demo__bar--1 {
    animation: demo-shift 2.6s var(--ease-out) both;
  }

  .stage__slide.is-active .demo__bar--2 {
    animation: demo-shift 2.6s var(--ease-out) 0.18s both;
  }

  .stage__slide.is-active .demo__bar--3 {
    animation: demo-shift 2.6s var(--ease-out) 0.36s both;
  }

  .stage__slide.is-active .demo__links {
    animation: demo-links-fade 2.6s var(--ease) both;
  }

  /* 4 — the pin is placed and the thread unfolds beside it. */
  @keyframes demo-pin {
    0% { opacity: 0; scale: 0.4; }
    30% { opacity: 1; scale: 1.25; }
    45%, 100% { opacity: 1; scale: 1; }
  }

  @keyframes demo-bubble {
    0%, 32% { opacity: 0; translate: -14px 0; scale: 0.96; }
    62%, 100% { opacity: 1; translate: 0 0; scale: 1; }
  }

  @keyframes demo-typing {
    0%, 55% { opacity: 0; }
    100% { opacity: 1; }
  }

  .stage__slide.is-active .demo__pin {
    transform-origin: 316px 104px;
    animation: demo-pin 2.4s var(--ease-out) both;
  }

  .stage__slide.is-active .demo__bubble {
    transform-origin: 368px 140px;
    animation: demo-bubble 2.4s var(--ease-out) both;
  }

  .stage__slide.is-active .demo__typing {
    animation: demo-typing 2.6s var(--ease) both;
  }

  /* 5 — the clock runs and the logged-hours bar fills to match. */
  @keyframes demo-hand-min {
    from { rotate: 0deg; }
    to { rotate: 360deg; }
  }

  @keyframes demo-hand-hr {
    from { rotate: 0deg; }
    to { rotate: 60deg; }
  }

  @keyframes demo-fill {
    from { scale: 0 1; }
    to { scale: 1 1; }
  }

  .stage__slide.is-active .demo__hand-min {
    transform-origin: 500px 150px;
    animation: demo-hand-min 2.4s linear infinite;
  }

  .stage__slide.is-active .demo__hand-hr {
    transform-origin: 500px 150px;
    animation: demo-hand-hr 4.8s linear both;
  }

  .stage__slide.is-active .demo__fill {
    transform-origin: 59px 277px;
    animation: demo-fill 2.6s var(--ease-out) both;
  }
}

/* --------------------------------------------------------- Alternating rows */

.frow__grid {
  display: grid;
  gap: clamp(var(--s-6), 5vw, var(--s-16));
  align-items: center;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
}

.frow--flip .frow__grid {
  direction: rtl;
}

.frow--flip .frow__grid > * {
  direction: ltr;
}

.frow__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-4);
}

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

.frow__list li {
  position: relative;
  padding-inline-start: var(--s-7);
  color: var(--ink-70);
  font-weight: 500;
}

.frow__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.45em;
  width: 12px;
  height: 7px;
  border-inline-start: 2.5px solid var(--accent);
  border-bottom: 2.5px solid var(--accent);
  rotate: -45deg;
}

/* --------------------------------------------------------------------------
   Bento grid

   Twelve columns, cards spanning seven or five. The uneven split is the whole
   point: an even grid of identical tiles reads as a specification sheet, and
   nobody reads a specification sheet. Alternating widths give the eye somewhere
   to rest and make the row feel arranged rather than generated.

   `auto-fit` is deliberately not used here. It would collapse the two spans to
   the same width at most viewports and take the rhythm with it.
   -------------------------------------------------------------------------- */

.bento {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(12, 1fr);
}

.bento__card {
  grid-column: span 12;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--fast) var(--ease), transform var(--fast) var(--ease);
}

.bento__card:hover {
  box-shadow: var(--shadow-2);
  transform: translate(-2px, -2px);
}

/* The media sits at the top and takes the width of the card, so a wide card
   gets a wide still and a narrow one gets a narrow still. That difference is
   what stops the grid from looking like a table of thumbnails. */
.bento__card .ph__art {
  border-radius: var(--r-md);
}

@media (min-width: 760px) {
  .bento__card--wide {
    grid-column: span 7;
  }

  .bento__card--narrow {
    grid-column: span 5;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bento__card:hover {
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   Homepage showcase.

   Same twelve-column rhythm as .bento, but each card carries a small mock-up
   of the product instead of a tinted placeholder, and the reveal sits on the
   card rather than on the grid — so the nine arrive one after another, and
   each starts its own parts moving as it lands.

   Kept apart from .bento rather than folded into it with a modifier: the two
   differ in what reveals and in whether the art moves, so every shared rule
   would have needed a qualifier anyway. The catalogue at /features stays on
   .bento, where a still tile per row is the right amount of picture.
   -------------------------------------------------------------------------- */

.show {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(12, 1fr);
}

.show__card {
  grid-column: span 12;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--fast) var(--ease), transform var(--fast) var(--ease);
}

.show__card:hover {
  box-shadow: var(--shadow-2);
  transform: translate(-2px, -2px);
}

.show__card .ph__art {
  border-radius: var(--r-md);
}

/* Column counts, not names: the showcase works out its own packing so every
   row comes to twelve, and a class per width is what that packing writes. */
@media (min-width: 760px) {
  .show__card--c5 {
    grid-column: span 5;
  }

  .show__card--c7 {
    grid-column: span 7;
  }

  .show__card--c12 {
    grid-column: span 12;
  }
}

/* The full-width card turns on its side rather than growing.

   Stacked, its art would be the width of the container at a 16:9 crop — well
   over six hundred pixels tall, a single picture taller than the viewport on a
   laptop. Set beside its own text instead, it stays the size of the cards
   around it and reads as the largest one rather than the loudest. */
@media (min-width: 900px) {
  .show__card--c12 {
    display: grid;
    grid-template-columns: 1.3fr 1fr;
    grid-template-rows: auto auto;
    align-items: center;
    column-gap: var(--s-7);
  }

  .show__card--c12 .ph {
    grid-row: 1 / span 2;
  }

  .show__card--c12 h3 {
    align-self: end;
  }

  .show__card--c12 p {
    align-self: start;
    margin-top: var(--s-3);
  }
}

/* The moving parts inside a mock-up.

   Everything is gated twice: on .reveal-ready, so a page whose script never
   ran shows the drawings complete rather than half-built, and on .is-visible,
   so nothing plays before it can be watched. Both gates matter — the first is
   the failsafe, the second is the reason the card is worth scrolling to.

   No animation loops forever. The observer unobserves after the first
   intersection, so .is-visible never comes off again, and an infinite
   keyframe here would leave the homepage moving for as long as the tab is
   open. The few that repeat are counted out and then rest. */
@media (prefers-reduced-motion: no-preference) {
  .reveal-ready .mk .mk-drop,
  .reveal-ready .mk .mk-rise,
  .reveal-ready .mk .mk-in,
  .reveal-ready .mk .mk-pin {
    opacity: 0;
  }

  .reveal-ready .show__card.is-visible .mk-drop {
    animation: mk-drop 620ms var(--ease) var(--d, 0ms) both;
  }

  .reveal-ready .show__card.is-visible .mk-rise {
    animation: mk-rise 620ms var(--ease) var(--d, 0ms) both;
  }

  .reveal-ready .show__card.is-visible .mk-in {
    animation: mk-in 620ms var(--ease) var(--d, 0ms) both;
  }

  .reveal-ready .show__card.is-visible .mk-pin {
    animation: mk-pin 520ms var(--ease) var(--d, 0ms) both;
  }

  /* The Gantt bars. Each one starts further left than it belongs and slides
     into place after the one above it — which is the card's whole claim. */
  .reveal-ready .show__card.is-visible .mk-shift {
    animation: mk-shift 700ms var(--ease) var(--d, 0ms) both;
  }

  .reveal-ready .show__card.is-visible .mk-fill {
    animation: mk-fill 1200ms var(--ease) 300ms both;
  }

  .reveal-ready .show__card.is-visible .mk-run {
    animation: mk-run 2800ms linear 250ms both;
  }

  .reveal-ready .show__card.is-visible .mk-pan {
    animation: mk-pan 3600ms var(--ease) 400ms 2 alternate both;
  }

  .reveal-ready .show__card.is-visible .mk-turn {
    animation: mk-turn 3200ms var(--ease) 300ms 3 both;
  }

  .reveal-ready .show__card.is-visible .mk-tick {
    animation: mk-tick 1200ms steps(1, end) 300ms 5 both;
  }
}

/* Scaling and rotation inside an SVG need the element's own box as the
   reference; without fill-box the origin is the whole viewport and the shape
   swings off the card instead of turning on the spot. */
.mk-fill {
  transform-box: fill-box;
  transform-origin: left center;
}

.mk-turn {
  transform-box: fill-box;
  transform-origin: center;
}

.mk-pin {
  transform-box: fill-box;
  transform-origin: center;
}

@keyframes mk-drop {
  from { opacity: 0; translate: 0 -26px; }
  to { opacity: 1; translate: 0 0; }
}

@keyframes mk-rise {
  from { opacity: 0; translate: 0 22px; }
  to { opacity: 1; translate: 0 0; }
}

@keyframes mk-in {
  from { opacity: 0; translate: -30px 0; }
  to { opacity: 1; translate: 0 0; }
}

@keyframes mk-pin {
  from { opacity: 0; scale: 0.4; }
  to { opacity: 1; scale: 1; }
}

@keyframes mk-shift {
  from { translate: -46px 0; }
  to { translate: 0 0; }
}

@keyframes mk-fill {
  from { scale: 0 1; }
  to { scale: 1 1; }
}

@keyframes mk-run {
  from { translate: 0 0; }
  to { translate: 500px 0; }
}

/* A viewport crossing a board that is larger than it. */
@keyframes mk-pan {
  from { translate: 0 0; }
  to { translate: 262px 58px; }
}

/* Squeezing the width reads as a turn on a turntable. A true rotation would
   spin the cube like a wheel, which is not what turning a model looks like. */
@keyframes mk-turn {
  0%, 100% { scale: 1 1; }
  50% { scale: 0.84 1; }
}

@keyframes mk-tick {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.2; }
}

@media (prefers-reduced-motion: reduce) {
  .show__card:hover {
    transform: none;
  }
}

/* Plan pager. Sits under the row, centred, and app.js only unhides it when
   the row genuinely overflows. Arrows floating at the left and right edges
   would sit on top of the outermost plan cards and cover the price, which is
   the one thing nobody should have to scroll past a button to read. */
.plans-nav {
  display: flex;
  justify-content: center;
  gap: var(--s-3);
  margin-top: var(--s-6);
}

.plans-nav[hidden] {
  display: none;
}

/* The billing controls read as one row: seats, then the period. Centred,
   because they govern every card below them equally and a left-aligned
   control implies it belongs to the first one. */
.price-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
}

.seat-over {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  max-width: 68ch;
  margin: var(--s-6) auto 0;
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--tile-sun);
  box-shadow: var(--shadow-1);
  text-align: center;
}

.seat-over[hidden] {
  display: none;
}

/* --------------------------------------------------------------------------
   Seat selector

   The native dropdown arrived styled by the operating system: a grey chrome
   control in the middle of a page made of ink outlines. `appearance: none`
   plus our own chevron puts it back in the same family as every other
   control here.

   The list itself (the popup) is still drawn by the OS and cannot be styled.
   That is the correct trade: a JavaScript listbox would have to reimplement
   type-ahead, screen-reader semantics and mobile pickers, and would be worse
   at all three.
   -------------------------------------------------------------------------- */

.price-controls select,
select[data-seats] {
  appearance: none;
  -webkit-appearance: none;
  padding: 0.55rem 2.4rem 0.55rem var(--s-4);
  border: var(--stroke);
  border-radius: var(--r-round);
  background-color: var(--surface-2);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23171512' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-4) center;
  background-size: 16px 16px;
  box-shadow: var(--shadow-1);
  font: inherit;
  font-weight: 650;
  color: var(--ink);
  cursor: pointer;
}

select[data-seats]:hover {
  background-color: var(--surface);
}

/* The list price, kept visible next to the discounted one. */
.plan__was {
  color: var(--ink-50);
  text-decoration-thickness: 2px;
}

.plan__off {
  display: inline-block;
  padding: 0.05rem var(--s-2);
  border: 1.5px solid var(--ink);
  border-radius: var(--r-round);
  background: var(--pop-sun);
  font-family: var(--mono);
  font-size: var(--t-label);
  font-weight: 700;
  color: var(--ink);
}

/* The tier table under the controls. Stated on the page rather than only
   appearing once the right number is picked — a discount you have to
   discover by clicking is a discount most people never see. */
.tiers {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3);
  margin-top: var(--s-5);
}

.tiers__item {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  padding: 0.35rem var(--s-4);
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  font-size: var(--t-sm);
}

.tiers__off {
  font-family: var(--mono);
  font-weight: 700;
  color: var(--accent-strong);
}

/* The FAQ sits in a narrow, centred column. Questions and answers are read
   one after another rather than scanned, and a 1200px line is too far for the
   eye to travel back to the next question. */
.faq {
  max-width: 62rem;
  margin-inline: auto;
}

.faq + .faq-note,
.section-head--faq {
  text-align: center;
}

/* --------------------------------------------------------------------------
   Price transitions

   The number changes when the period or the seat count changes, and a figure
   that swaps instantly reads as a glitch — people re-read it to check they saw
   it right. A short lift and fade says "this just updated" without making
   anyone wait for it.

   Driven by a class that app.js adds and removes on animationend, so it can
   restart on every change instead of being swallowed by a running timer.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @keyframes price-in {
    from {
      opacity: 0;
      translate: 0 6px;
    }
  }

  .plan__price.is-updated,
  [data-plan-total].is-updated {
    animation: price-in 260ms var(--ease) both;
  }

  /* The selected period slides rather than jumps. The pill is a pseudo element
     on the group, so one transition covers both buttons. */
  .billing-toggle button {
    transition: color var(--fast) var(--ease);
  }
}

/* The optional enterprise fields. `hidden` does the hiding; this only stops a
   flex/grid parent from overriding it. */
[data-enterprise-only][hidden] {
  display: none;
}

/* --------------------------------------------------------------------------
   5. Typography helpers
   -------------------------------------------------------------------------- */

/* Eyebrow: the Webflow section label — uppercase, bold, letter-spaced. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.eyebrow::before {
  content: none;
}

.eyebrow--plain::before {
  display: none;
}

/* The two top-of-page headings, and the only things that use the display
   face. Weight stays 400 because Gasoek One has exactly one — asking for
   more makes the browser fake it. */
.h0 {
  font-family: var(--font-display);
  font-size: var(--t-h0);
  font-weight: 400;
  line-height: 0.94;
  letter-spacing: -0.012em;
}

.h1 {
  font-family: var(--font-display);
  font-size: var(--t-h1);
  font-weight: 400;
  line-height: 0.98;
  letter-spacing: -0.01em;
}

.h2 {
  font-size: var(--t-h2);
  font-weight: 800;
  line-height: 1.06;
  letter-spacing: -0.03em;
}

.h3 {
  font-size: var(--t-h3);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.h4 {
  font-size: var(--t-h4);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.02em;
}

.lead {
  font-size: clamp(1rem, 0.9rem + 0.7vw, 1.375rem);
  line-height: 1.6;
  color: var(--ink-70);
  font-weight: 500;
}

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

.small {
  font-size: var(--t-sm);
}

.mono {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.01em;
}

.measure {
  max-width: 62ch;
}

.measure-narrow {
  max-width: 48ch;
}

.section-head {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}

.section-head .h2 {
  max-width: 24ch;
}

.section-head .lead {
  max-width: 58ch;
}

.section-head--center {
  align-items: center;
  text-align: center;
}

.section-head--center .h2,
.section-head--center .lead {
  margin-inline: auto;
}

/* A centred heading with a figure beside it.

   `.heading-with-doodle` wraps, which is right when the heading is a single
   line that might not fit — but on a centred two-line heading it dropped the
   figure onto a third line, hard against the left edge, where it read as a
   stray drawing rather than as punctuation on the title. Centred, and on one
   line with the words: the heading is free to wrap inside its own box while
   the figure stays beside it. Below the breakpoint where that gets tight the
   original wrapping comes back. */
.section-head--center .heading-with-doodle {
  justify-content: center;
  flex-wrap: nowrap;
  text-align: left;
}

/* Dasselbe im Kopfbereich einer Seite: auch dort steht die Überschrift
   mittig, und auch dort fiel die Figur sonst auf eine eigene Zeile am linken
   Rand. */
.hero__inner .heading-with-doodle {
  justify-content: center;
  flex-wrap: nowrap;
  text-align: left;
}

@media (max-width: 620px) {
  .section-head--center .heading-with-doodle,
  .hero__inner .heading-with-doodle {
    flex-wrap: wrap;
    justify-content: center;
    text-align: center;
  }
}

/* --------------------------------------------------------------------------
   6. Buttons — the Webflow pair: black primary, glass secondary.
   Both uppercase, 8px radius, lift + shadow on hover.
   -------------------------------------------------------------------------- */

/* Every button is a drawn object: ink outline, hard offset, and a press
   that drops it into its own shadow. */
.btn {
  --btn-bg: var(--surface-2);
  --btn-fg: var(--ink);
  --btn-line: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: 12px 24px;
  min-height: 44px;
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: var(--stroke-w) solid var(--btn-line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  line-height: 1;
  /* `nowrap` alone made a long label push the button past the viewport — on
     the calculator pages one came out 380px wide inside a 375px phone and
     scrolled the whole document sideways. The label still prefers one line;
     it is now allowed to wrap rather than to overflow, and the button can
     never be wider than the space it sits in. */
  text-wrap: balance;
  max-width: 100%;
  transition: background var(--fast) var(--ease),
    border-color var(--fast) var(--ease), color var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease), translate var(--fast) var(--ease);
}

/* Hover lifts by exactly the shadow offset, the press drops into it. */
.btn:hover {
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.btn:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

.btn--primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--on-accent);
  /* The outline stays ink even on a filled button: a violet-on-violet
     edge would drop it out of the system everything else belongs to. */
  --btn-line: var(--ink);
  font-weight: 800;
}

.btn--primary:hover {
  --btn-bg: var(--accent-hover);
}

.btn--ghost {
  --btn-bg: var(--surface-2);
}

.btn--ghost:hover {
  --btn-bg: var(--surface);
}

.btn--quiet {
  --btn-bg: transparent;
  --btn-line: transparent;
  padding-inline: var(--s-3);
  box-shadow: none;
}

.btn--quiet:hover,
.btn--quiet:active {
  box-shadow: none;
  translate: 0 0;
}

.btn--quiet:hover {
  --btn-bg: var(--ink-06);
}

@media (prefers-reduced-motion: reduce) {
  .btn:hover,
  .btn:active {
    translate: 0 0;
  }
}

.btn--lg {
  padding: 16px 32px;
  min-height: 52px;
  font-size: 15px;
}

.btn--block {
  width: 100%;
  /* Full-width buttons carry generated labels of unknown length; a nowrap
     button overflows in half the locales. */
  white-space: normal;
  text-align: center;
  line-height: 1.3;
}

/* Arrow link — the arrow steps forward on hover, nothing else moves. */
.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-weight: 700;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--ink);
}

.arrow-link:hover {
  color: var(--accent-strong);
}

.arrow-link svg,
.arrow-link .arrow {
  transition: translate var(--base) var(--ease);
}

.arrow-link:hover svg,
.arrow-link:hover .arrow {
  translate: 4px 0;
}

/* --------------------------------------------------------------------------
   7. Tags and badges — the hero pill and module tags.
   -------------------------------------------------------------------------- */

.tag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 16px;
  border: var(--stroke);
  border-radius: var(--r-round);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-70);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.tag--accent {
  color: var(--ink);
  background: var(--tile-purple);
}

.tag--soon {
  border-color: var(--line);
  color: var(--ink-60);
  background: transparent;
  box-shadow: none;
}

/* The hero pill is a sticker: tinted fill, ink outline, hard offset. */
.hero .tag {
  color: var(--ink);
  background: var(--tile-purple);
  border-color: var(--ink);
}

.dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.dot--live {
  background: #10b981;
  box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}

/* --------------------------------------------------------------------------
   8. Header — white, blurred, hairline underneath. Logo is the wordmark.
   -------------------------------------------------------------------------- */

/* The header closes on a wave, related to the footer's but not the same
   one: seven short bows rather than two long ones. The footer's swing is
   the page's full stop and can afford the length; up here the bar is
   68px of furniture the visitor scrolls past, and a long low curve read
   as a wobble in a straight line rather than as a wave. Short wavelength
   keeps that rhythm; the amplitude carries the swing, and at 16 units in
   a 34-unit box the bows are deep enough to read as drawn rather than as
   a printing defect, while still clearing the headline underneath.

   Built with smooth-curve commands, which mirror the previous control
   point — that is what keeps every crossing tangent-continuous, so the
   line stays a wave instead of a row of scallops. */
.header {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  background: var(--accent);
  border-bottom: 0;
}

/* The wave is an extension of the bar's own surface, so it repeats the
   background and the blur rather than sitting on top of them — anything
   else shows a seam exactly where the two meet. It hangs below the bar,
   which is why it may not intercept clicks. */
.header::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 100%;
  height: 16px;
  background: var(--accent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 34' preserveAspectRatio='none'%3E%3Cpath d='M0 5c37.03 0 65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00 L1440 0 L0 0Z' fill='%23000'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 34' preserveAspectRatio='none'%3E%3Cpath d='M0 5c37.03 0 65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00 L1440 0 L0 0Z' fill='%23000'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  pointer-events: none;
}

/* The ink line along the crest, as its own masked layer so the surface
   above stays opaque where the line dips. */
.header::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 100%;
  height: 16px;
  background: var(--ink);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 34' preserveAspectRatio='none'%3E%3Cpath d='M0 5c37.03 0 65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00' fill='none' stroke='%23000' stroke-width='2' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 34' preserveAspectRatio='none'%3E%3Cpath d='M0 5c37.03 0 65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00s65.83 16.00 102.86 16.00s65.83 -16.00 102.86 -16.00' fill='none' stroke='%23000' stroke-width='2' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  pointer-events: none;
}

/* Deliberately nothing on scroll. A drop-shadow on the bar traces the whole
   silhouette, so it landed directly beneath the ink crest and read as that
   line growing from 2px to 5px — the bar appeared to gain weight the moment
   the page moved. The crest already separates the bar from the page at every
   scroll position, so there is nothing left for a shadow to say. */

.header__inner {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The Webflow logo is a pure text wordmark: CEYU, 26px, 800. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  font-weight: 800;
  font-size: 26px;
  letter-spacing: -0.5px;
  text-transform: uppercase;
  color: var(--ink);
  flex: none;
}

.brand__mark {
  display: none;
}

/* Centred in the bar: `auto` on both sides splits the free space evenly, so
   the links sit midway between the wordmark and the account controls rather
   than hugging either end. */
.nav {
  display: flex;
  align-items: center;
  gap: clamp(4px, 1vw, 12px);
  margin-inline: auto;
}

/* Full ink rather than the old 60% grey, and a step larger. The row is short
   enough now to carry the weight without shouting over the brand.
   Black by default costs the two states that used to be carried by darkening
   towards ink — hover and current page had nowhere left to go — so both move
   to the accent instead. --accent-strong, not --accent: it is the pair that
   clears 7:1 on white, and these are small uppercase glyphs. */
.nav__link {
  position: relative;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink);
  transition: color var(--fast) var(--ease);
}

.nav__link:hover {
  color: var(--accent-strong);
  background: transparent;
}

.nav__link[aria-current="page"] {
  color: var(--accent-strong);
}

.nav__link[aria-current="page"]::after {
  content: none;
}

/* No auto margin here: the nav's own `margin-inline: auto` owns the free
   space now, and a third auto margin would split it three ways and pull the
   nav back off centre. */
.header__actions {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex: none;
}

.burger {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  border: var(--stroke);
  border-radius: var(--r-sm);
  flex: none;
}

.burger__box {
  display: block;
  width: 18px;
  height: 12px;
  position: relative;
}

.burger__box span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--ink);
  border-radius: 2px;
  transition: transform var(--base) var(--ease), opacity var(--fast) var(--ease);
}

.burger__box span:nth-child(1) {
  top: 0;
}
.burger__box span:nth-child(2) {
  top: 50%;
  translate: 0 -50%;
}
.burger__box span:nth-child(3) {
  bottom: 0;
}

.burger[aria-expanded="true"] .burger__box span:nth-child(1) {
  transform: translateY(5px) rotate(45deg);
}
.burger[aria-expanded="true"] .burger__box span:nth-child(2) {
  opacity: 0;
}
.burger[aria-expanded="true"] .burger__box span:nth-child(3) {
  transform: translateY(-5px) rotate(-45deg);
}

/* Mobile drawer */
.mobile-nav {
  position: fixed;
  inset: var(--mobile-nav-top, var(--header-h)) 0 0;
  z-index: 99;
  background: var(--ground);
  border-top: 1px solid var(--line);
  padding: var(--s-6) var(--gutter) var(--s-12);
  overflow-y: auto;
  display: none;
  flex-direction: column;
  gap: var(--s-2);
}

.mobile-nav[data-open="true"] {
  display: flex;
}

.mobile-nav a {
  padding: var(--s-4) var(--s-2);
  border-bottom: 1px solid var(--line);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink);
}

.mobile-nav .btn {
  margin-top: var(--s-5);
}

body[data-nav-open="true"] {
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   9. Hero — fully centered like the Webflow build, media card below.
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  padding-block: clamp(5rem, 12vw, 8.75rem) clamp(2.5rem, 8vw, 5rem);
  overflow: hidden;
}

.hero__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0;
}

.hero__inner > * + * {
  margin-top: var(--s-6);
}

/* Capped in characters, not pixels: the headline is meant to break into
   three near-full lines, and a character cap holds that shape when the
   copy is translated. The display face is far wider than the body face,
   so the cap is tighter than it would be for Figtree. */
.hero__title {
  max-width: 13ch;
  font-weight: 400;
  letter-spacing: -0.012em;
  line-height: 0.94;
}

.hero__title--big {
  font-size: clamp(2.5rem, 1.45rem + 4.3vw, 4.25rem);
}

.hero__title em {
  font-style: normal;
  color: var(--ink);
}

.hero__lead {
  max-width: 750px;
  font-size: clamp(1rem, 0.85rem + 0.9vw, 1.375rem);
  font-weight: 500;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3);
}

.hero__note {
  display: inline-flex;
  align-items: center;
  width: fit-content;
  padding: 0;
  border: 0;
  background: transparent;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--ink-60);
}

/* Product frame under the hero — the Webflow hero-media card. */
.hero__frame {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 1000px;
  margin: clamp(2.5rem, 5vw, 3.75rem) auto 0;
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}

.frame__bar {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: var(--stroke);
  background: var(--surface);
}

.frame__dots {
  display: flex;
  gap: 6px;
}

.frame__dots i {
  width: 10px;
  height: 10px;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background: var(--tile-purple);
}

.frame__label {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.frame__body {
  position: relative;
  aspect-ratio: 16 / 9;
  background-image: linear-gradient(var(--ink-06) 1px, transparent 1px),
    linear-gradient(90deg, var(--ink-06) 1px, transparent 1px);
  background-size: 28px 28px;
}

/* --------------------------------------------------------------------------
   10. Marquee — the audience ticker: big bold names with purple stars.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Audience band — a live marquee

   It ran once, was stopped because a band moving on every page is motion the
   reader did not ask for, and is running again — with the thing that was
   missing the first time: it stops the moment the pointer is on it, so a name
   can always be read, and it stops for anyone who asked for reduced motion.

   The track is duplicated by app.js and translated by exactly half its width,
   which is what makes the loop seamless: the copy arrives where the original
   started. Nothing is measured in pixels here, so the band does not have to be
   re-tuned when a word is translated into a longer one.
   -------------------------------------------------------------------------- */

.marquee {
  position: relative;
  overflow: hidden;
  padding-block: var(--s-4) var(--s-4);
  border-block: var(--stroke);
  /* The brand violet, not a tint of it. A pale strip between two white
     sections reads as a gap the page forgot to fill; a full-strength band
     reads as a statement, which is what a list of who this is for should be. */
  background: var(--accent);
  color: #ffffff;
}

/* The words run out of view rather than being cut off at the border. The mask
   belongs to the window, not to the band and not to the track: on the band it
   faded the eyebrow's ends too, and on the track — several times wider than
   the band — the fade sat off screen entirely. */
.marquee__viewport {
  overflow: hidden;
  mask: linear-gradient(
    to right,
    transparent 0,
    #000 7%,
    #000 93%,
    transparent 100%
  );
}

.marquee__track {
  display: flex;
  width: max-content;
  align-items: center;
  column-gap: 60px;
  animation: marquee-run var(--marquee-dur, 44s) linear infinite;
}

/* Hovering holds it. `animation-play-state` rather than a class, so the band
   picks up exactly where it stopped instead of jumping back to the start. */
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track {
  animation-play-state: paused;
}

@keyframes marquee-run {
  from {
    translate: 0 0;
  }
  to {
    translate: -50% 0;
  }
}

/* The line that says what the band is. Without it the row of professions is
   a list with no sentence in front of it. */
.marquee__eyebrow {
  margin-bottom: var(--s-3);
  color: rgba(255, 255, 255, 0.86);
  font-size: var(--t-label);
  font-weight: 800;
  letter-spacing: 2px;
  text-transform: uppercase;
  text-align: center;
}

.marquee__item {
  display: inline-flex;
  align-items: center;
  padding: 2px 4px;
  font-size: clamp(18px, 2.5vw, 24px);
  font-weight: 800;
  letter-spacing: -0.5px;
  text-transform: uppercase;
  color: #ffffff;
  white-space: nowrap;
  transition: color var(--fast) var(--ease), scale 260ms var(--ease-out),
    opacity 260ms var(--ease);
}

/* One name at a time: the one under the pointer comes forward in ink and the
   rest step back, so stopping the band also picks something out of it. */
.marquee:hover .marquee__item {
  opacity: 0.42;
}

.marquee .marquee__item:hover {
  opacity: 1;
  color: var(--pop-sun);
  scale: 1.06;
}

@media (prefers-reduced-motion: reduce) {
  .marquee__track {
    animation: none;
    flex-wrap: wrap;
    width: auto;
    justify-content: center;
    row-gap: var(--s-3);
  }

  .marquee__item {
    transition: none;
  }

  .marquee .marquee__item:hover {
    scale: 1;
  }
}

/* --------------------------------------------------------------------------
   11. Cards — the Webflow bento card.
   -------------------------------------------------------------------------- */

.card {
  position: relative;
  padding: clamp(24px, 3vw, 32px);
  background: var(--surface-2);
  border: var(--stroke);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--base) var(--ease),
    background var(--base) var(--ease), translate var(--base) var(--ease);
}

/* Hover raises the card by exactly its shadow offset. The border stays
   ink: fading it to an accent reads as the card losing focus. */
.card:hover {
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

@media (prefers-reduced-motion: reduce) {
  .card:hover {
    translate: 0 0;
  }
}

.card__icon {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-bottom: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--tile-purple);
  color: var(--ink);
  box-shadow: none;
  transition: background var(--base) var(--ease), color var(--base) var(--ease);
}

.card:hover .card__icon {
  background: var(--accent);
  color: var(--on-accent);
}

.card__icon svg {
  width: 20px;
  height: 20px;
}

.card__title {
  font-size: clamp(1.125rem, 1rem + 0.5vw, 1.5rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-bottom: var(--s-2);
}

.card__text {
  color: var(--ink-60);
  font-size: clamp(0.875rem, 0.8rem + 0.3vw, 1.0625rem);
  font-weight: 500;
  line-height: 1.6;
}

.card--link:hover {
  translate: -2px -2px;
}

/* Feature grid — all cards white, like the Webflow bento. */
.features-grid {
  display: grid;
  gap: 24px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}

.features-grid > .card:nth-child(4n + 2) {
  background: var(--tile-sky);
}

.features-grid > .card:nth-child(4n + 3) {
  background: var(--tile-coral);
}

.features-grid > .card:nth-child(4n + 4) {
  background: var(--tile-mint);
}

/* A wide two-up where the first item spans more */
.split {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  grid-template-columns: 1fr;
}

@media (min-width: 900px) {
  .split {
    grid-template-columns: 1.05fr 1fr;
  }

  .split--reverse > :first-child {
    order: 2;
  }
}

/* --------------------------------------------------------------------------
   12. Spec list — dense capability rows
   -------------------------------------------------------------------------- */

.spec-group + .spec-group {
  margin-top: var(--s-10);
}

.spec-group__title {
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.spec-group__title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

.spec-list {
  display: grid;
  gap: var(--s-4) var(--s-8);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}

.spec {
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
}

.spec__name {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-weight: 700;
  font-size: var(--t-sm);
  margin-bottom: var(--s-1);
}

.spec__text {
  font-size: var(--t-sm);
  color: var(--ink-60);
  font-weight: 500;
}

/* --------------------------------------------------------------------------
   13. Pricing — ported from the Webflow pricing grid.
   -------------------------------------------------------------------------- */

/* Billing toggle: grey track, white active pill. */
.billing-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

/* The selection is one pill that travels, not a fill that switches on one
   button and off the other. app.js measures the pressed button and writes
   --thumb-x / --thumb-w, because the two labels are different widths — a
   hard-coded half would sit off its word in English and further off in
   German. Width 0 until that first measurement lands, and .is-ready is only
   added afterwards, so the pill never animates in from nowhere on load. */
.billing-toggle::before {
  content: "";
  position: absolute;
  z-index: 0;
  top: 4px;
  bottom: 4px;
  left: 0;
  width: var(--thumb-w, 0);
  translate: var(--thumb-x, 0) 0;
  border-radius: var(--r-round);
  background: var(--ink);
  pointer-events: none;
}

.billing-toggle.is-ready::before {
  transition: translate var(--base) var(--ease-out),
    width var(--base) var(--ease-out);
}

.billing-toggle button {
  position: relative;
  z-index: 1;
  padding: 12px 24px;
  border-radius: var(--r-round);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
  transition: color var(--fast) var(--ease);
}

.billing-toggle button:hover {
  color: var(--ink);
}

/* The pill underneath supplies the fill; the label only changes colour, and
   it changes faster than the pill moves so the word is already legible by the
   time the pill arrives under it. */
.billing-toggle button[aria-pressed="true"] {
  color: var(--ground);
}

@media (prefers-reduced-motion: reduce) {
  .billing-toggle.is-ready::before {
    transition-duration: 0.01ms;
  }
}

.billing-toggle .save {
  font-size: 11px;
  font-weight: 800;
  padding: 2px 6px;
  border-radius: 4px;
  border: var(--stroke);
  background: var(--pop-sun);
  color: var(--ink);
  margin-inline-end: var(--s-2);
}

/* Controls row: seat selector + billing toggle side by side, centred. */
.pricing-controls {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 20px;
}

/* Seat selector — a drawn pill that opens a listbox of our own. The popup is
   a real element, so it takes the ink outline and the offset shadow the rest
   of the page uses; a <select> menu is drawn by the operating system and
   takes neither. */
.seat-select {
  display: inline-flex;
  position: relative;
  background: var(--surface-2);
  border: var(--stroke);
  border-radius: var(--r-round);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

.seat-select:hover {
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.seat-select__combo {
  display: inline-flex;
  align-items: center;
  /* The discount badge is a second object, not part of the number. At the
     8px this used to sit at, "90 seats" and the badge read as one smudged
     word; a badge needs air on both sides to be a badge. */
  gap: var(--s-3);
  padding: 12px 16px 12px 20px;
  font-size: 14px;
  font-weight: 700;
  color: var(--ink);
  cursor: pointer;
  user-select: none;
}

/* The global ring already applies; only its corner needs to follow the pill. */
.seat-select__combo:focus-visible {
  border-radius: var(--r-round);
}

.seat-select__chevron {
  flex: none;
  transition: rotate var(--fast) var(--ease);
}

.seat-select__combo[aria-expanded="true"] .seat-select__chevron {
  rotate: 180deg;
}

@media (prefers-reduced-motion: reduce) {
  .seat-select__chevron {
    transition: none;
  }
}

/* The collapsed value and every row share this pair, so "25 seats −5%" reads
   the same whether the list is open or shut.

   The gap belongs here, not on the combo: the number and the badge are both
   children of this span, and the combo's own gap only ever separated this
   span from the chevron. That is why the badge sat flush against the "s" of
   "seats" no matter what the combo was set to. */
.seat-select__value {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
}

.seat-select__n {
  font-variant-numeric: tabular-nums;
}

.seat-select__off {
  font-size: 11px;
  font-weight: 800;
  padding: 3px 8px;
  border-radius: 6px;
  border: var(--stroke);
  background: var(--pop-sun);
  color: var(--ink);
}

/* Moved to <body> and positioned by app.js. The hero it sits in clips its
   overflow, and the reveal animation leaves `will-change: translate` on the
   controls row, which makes even a fixed child a clipped child. Sitting on
   the body sidesteps both without touching either. */
.seat-select__list {
  position: absolute;
  z-index: 200;
  max-height: min(320px, 50vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  padding: var(--s-2);
  list-style: none;
  background: var(--surface-2);
  border: var(--stroke);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
}

.seat-select__option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-2) var(--s-3);
  border-radius: 6px;
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-70);
  cursor: pointer;
  white-space: nowrap;
}

/* Hover and keyboard land on the same highlight: the pointer sets the active
   option too, so there is never a second, competing one. */
.seat-select__option.is-active {
  background: var(--surface);
  color: var(--ink);
}

.seat-select__option[aria-selected="true"] {
  color: var(--ink);
  font-weight: 800;
}

/* Per-seat total line under the price. */
.plan__total {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent-strong);
  font-variant-numeric: tabular-nums;
}

/* Five plans: explicit columns on desktop, swipe track below.

   The recommended column is wider than the other four, and the card in it
   also grows past the top and bottom of the row (see .plan--featured). Size
   is the part of a recommendation that works before anything is read: the
   eye lands on the largest object in the row, and only then reads what it
   says. The badge, the violet fill and the deeper shadow all agree with it
   rather than each carrying the message alone.

   The row still has to fit: 1.34fr against four 1fr columns leaves the four
   at about 216px inside a 1400px container, which is enough for a price and
   a four-line feature list. */
.plans {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(2, minmax(0, 1fr)) minmax(0, 1.34fr) repeat(
      2,
      minmax(0, 1fr)
    );
  align-items: stretch;
  /* Room for the featured card to hang below the row without the section
     below it moving up into the shadow. */
  padding-bottom: 22px;
}

@media (max-width: 1299px) {
  .plans {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--gutter);
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    /* `overflow-x: auto` makes overflow-y compute to auto as well, so this
       scroller clips vertically whether we want it to or not. The recommended
       card's badge hangs 16px above the card edge (plus its own border and
       shadow), and 12px of headroom sliced the top off it on every screen
       narrow enough to reach this layout. The top padding is tied to the same
       token the badge offset uses, so the two cannot drift apart again. */
    padding-block: calc(var(--plan-badge-rise) + 10px) var(--s-4);
    padding-inline: 2px;
    margin-inline: -2px;
  }

  .plans > .plan {
    flex: 0 0 min(300px, 88%);
    scroll-snap-align: center;
  }

  /* The swipe track cannot make a card taller than the row, so the
     recommended one is marked by width alone down here. */
  .plans > .plan--featured {
    flex: 0 0 min(340px, 92%);
  }

  .plans {
    scrollbar-width: thin;
    scrollbar-color: var(--ink-20) transparent;
  }
  .plans::-webkit-scrollbar { height: 6px; }
  .plans::-webkit-scrollbar-thumb {
    background: var(--ink-20);
    border-radius: var(--r-round);
  }
  .plans::-webkit-scrollbar-track { background: transparent; }
}

/* No `height: 100%` here. An explicit height on a flex or grid item beats the
   container's `align-items: stretch`, and against an auto-height container the
   percentage resolved to the card's own content height — so below 1300px,
   where .plans becomes a flex row, the five cards ended up 450, 556, 591, 589
   and 533px tall. Letting stretch do the work makes them equal in both
   layouts, which is the whole point of stretch. */
.plan {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: 28px 22px 24px;
  transition: translate var(--base) var(--ease),
    box-shadow var(--base) var(--ease);
}

/* Equal-height cards alone do not line the buttons up: the feature lists have
   different lengths, so each call to action floated to wherever its list
   ended. Pushing it down with an auto margin puts every button on the same
   line, which is what makes the row read as one price table rather than five
   loose cards. */
.plan > .btn {
  margin-top: auto;
  /* Every card's call to action is the same box, top and bottom. The bottom
     edges already lined up — an auto margin does that much — but "Start free"
     is one line and "Checkout opens after Creem is connected" is three, so
     the tops sat 40px apart and the row read as five loose cards again. The
     tallest label decides the height and the rest centre inside it. */
  min-height: 80px;
  display: grid;
  place-items: center;
  text-align: center;
}

/* The small print belongs to the button, not to the feature list above it. The
   auto margin lifts the button to the shared baseline and would otherwise
   leave its own footnote stranded higher up the card. */
.plan__fine {
  order: 1;
}

/* The waiting-for-checkout label is the longest string on the row by some way.
   At the button's own size it ran to three lines and made those cards' buttons
   taller than the two with a real call to action — which is the alignment this
   block exists to produce. A step down in size fits it on two. */
.plan > .btn.is-disabled {
  font-size: 13px;
  line-height: 1.3;
}

.plan:hover {
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.plans > .plan {
  background: var(--surface-2);
}

/* The recommended plan: tinted, with a deeper offset than its neighbours.
   Colour carries exactly one message here — "this is the one most people
   want" — so every other plan sits quietly on white.
   Specificity, not source order, decides this: `.plans > .plan` above is
   0,2,1 and a bare `.plan--featured` is 0,1,0, so it would lose wherever
   it sits in the file. */
.plans > .plan--featured {
  background: var(--accent);
  border-width: 3px;
  box-shadow: var(--shadow-3);
  z-index: 1;
  color: #ffffff;
  /* Taller than the row, and roomier inside it — but downward only. It used
     to grow at both edges, which pushed its button 22px below the other four
     and broke the line the five calls to action are supposed to share. The
     extra 32px at the foot is added to the padding rather than left as slack,
     so the button still sits on the row's own baseline with the violet
     carrying on beneath it. */
  margin-block: 0 -32px;
  padding: 30px 28px 56px;
}

/* The larger card carries a larger price and a larger name — the difference
   has to survive being looked at rather than measured. */
.plans > .plan--featured .plan__name {
  font-size: 23px;
}

.plans > .plan--featured .plan__amount {
  font-size: clamp(2.1rem, 1.5rem + 2.2vw, 2.9rem);
}

.plans > .plan--featured .plan__features li {
  font-size: 14.5px;
}

/* It lifts like the others, but from where it already is. */
.plans > .plan--featured:hover {
  translate: -2px -4px;
}

/* Everything inside the recommended card is set on the violet, so the greys
   that work on white have to be restated — an --ink-60 caption on --accent
   is unreadable. White is 4.9:1 on this violet; the softened variants stay
   at 85% rather than dropping to a grey, which would fall under the floor.
   The tick marks keep their own colour cue by going to the mint, which is
   the only other thing on the card that is not white. */
.plans > .plan--featured .plan__name,
.plans > .plan--featured .plan__for,
.plans > .plan--featured .plan__price,
.plans > .plan--featured .plan__amount,
.plans > .plan--featured .plan__features,
.plans > .plan--featured .plan__features li {
  color: #ffffff;
}

/* The secondary lines are full white too, not a softened one. 85% white
   composites to rgb(238,228,250) over this violet, which is 3.9:1 — under
   the 4.5:1 floor for 13px text. On a fill this saturated there is no room
   between "white" and "too grey to read", so the hierarchy is carried by
   size and weight instead of by opacity. */
.plans > .plan--featured .plan__per,
.plans > .plan--featured .plan__billing,
.plans > .plan--featured .plan__total,
.plans > .plan--featured .plan__fine,
.plans > .plan--featured .sub {
  color: #ffffff;
}

/* The struck list price was left at --ink-40 — a 25%-opaque near-black that
   composites to a slightly darker violet on this fill and reads as nothing at
   all. It is the anchor the discount is measured against, so it has to be
   legible: white at 78% is 3.5:1 here, which is under the body-text floor and
   correct for it, because a struck-through price is deliberately the quieter
   half of the pair — but it is now visible, which it was not. */
.plans > .plan--featured .plan__was {
  color: rgba(255, 255, 255, 0.78);
  text-decoration-color: rgba(255, 255, 255, 0.78);
}

/* White, not the mint this used to carry. The mint measures 2.1:1 on this
   violet, and the tick is the only thing marking a line as included — it has
   to clear the text floor rather than settle for being visible. */
.plans > .plan--featured .plan__features li::before,
.plans > .plan--featured .plan__features li > svg {
  color: #ffffff;
}

/* The divider above the feature list was an ink hairline; on the violet it
   has to be drawn out of the card's own light instead. */
.plans > .plan--featured .plan__features {
  border-top-color: rgba(255, 255, 255, 0.35);
}

/* The POPULAR badge: Miro-yellow pill with a black outline and a hard
   offset shadow — the one sticker in the design. */
/* How far the badge rises above the card edge. A token because the scrolling
   plan row has to reserve exactly this much headroom, and a hard-coded pair
   of numbers in two places is how the badge got clipped in the first place. */
/* The one label that has to carry across a five-card row without being read
   as a footnote. At 11px it was smaller than the small print on the card it
   was meant to single out — and at 15px on a card that is now visibly larger
   than its neighbours it was doing a job the card already does. It sits a
   step under the plan name: noticed, not shouted. */
.plan__badge {
  position: absolute;
  top: calc(-1 * var(--plan-badge-rise));
  inset-inline: 0;
  width: max-content;
  margin-inline: auto;
  padding: 7px 18px;
  border: var(--stroke);
  background: var(--pop-sun);
  color: var(--ink);
  box-shadow: var(--shadow-1);
  border-radius: var(--r-round);
  font-size: 12.5px;
  font-weight: 800;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  white-space: nowrap;
  /* Never wider than the card it belongs to: the German label is half again
     as long as the English one, and a badge that outgrows its card sits over
     the two beside it. */
  max-width: calc(100% - var(--s-4));
  overflow: hidden;
  text-overflow: ellipsis;
  z-index: 2;
}

.plan__name {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.5px;
  text-transform: uppercase;
  color: var(--ink);
}

.plan__for {
  font-size: 14px;
  color: var(--ink-60);
  min-height: 2.8em;
  line-height: 1.5;
}

.plan__price {
  display: flex;
  align-items: baseline;
  gap: 4px;
  font-variant-numeric: tabular-nums;
}

.plan__amount {
  font-size: clamp(30px, 3.5vw, 42px);
  font-weight: 800;
  letter-spacing: -1px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.plan__per {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-60);
}

.plan__was {
  font-size: clamp(14px, 1.5vw, 16px);
  color: var(--ink-40);
  text-decoration: line-through;
}

.plan__billing {
  font-size: 12px;
  color: var(--ink-60);
}

.plan__features {
  display: grid;
  gap: 12px;
  margin-top: var(--s-1);
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
}

.plan__features li {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 6px;
  align-items: start;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-60);
}

.plan__features svg {
  width: 18px;
  height: 18px;
  margin-top: 2px;
  color: var(--accent);
  flex: none;
}

.plan__features .sub {
  /* The label row is a plain line, not an icon+text pair. */
  display: block;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
  grid-column: 1 / -1;
  margin-top: var(--s-2);
}

.plan .btn {
  margin-top: auto;
}

/* --------------------------------------------------------------------------
   14. Comparison table — canvas header row, featured column ringed black.
   -------------------------------------------------------------------------- */

/* The scroller is a rounded, outlined box, so whatever it contains has to
   respect that outline. It used to reserve 14px at the top for the "most
   popular" badge to straddle the edge — but the highlighted column is
   tinted and ink-bordered, so those 14px showed up as a white band above a
   column that otherwise ran to the edge, with a hard square top inside a
   --r-xl corner. The badge now sits inside the header cell instead (it has
   the padding for it), which lets the table start flush and lets the radius
   do its job.

   `overflow: clip` on both axes with `overflow-x: auto`: the scroll axis
   still scrolls, and the cross axis clips to the curve instead of leaking a
   square corner. */
.table-scroll {
  overflow-x: auto;
  overflow-y: clip;
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.cmp {
  min-width: 780px;
  font-size: 14px;
}

.cmp th,
.cmp td {
  padding: 16px 20px;
  text-align: center;
  border-bottom: 1px solid var(--line);
}

/* The header row stays white. An ink bar made all five columns shout at
   once, which defeats marking one of them — the violet column and its
   badge now carry that job alone. Sticky, because a wide table needs its
   header exactly when you have scrolled away from it. */
.cmp thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-2);
  border-bottom: 2px solid var(--ink);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}

/* Pro is the 4th column; marked by position so the 20 generated locales
   stay untouched. The fill itself is set on .cmp--full below — every plan
   table carries that class — so a two-column table that happens to use .cmp
   (the outage table on /status) is not given a violet column it has no use
   for. */
.cmp tbody td:nth-child(4) {
  font-weight: 700;
}

.cmp tbody tr:nth-child(even) td {
  background: var(--surface);
}

/* The label column keeps its own tint through the zebra, one step darker so
   the stripe still reads without the column losing its identity. */
.cmp tbody tr:nth-child(even) th:first-child {
  background: color-mix(in srgb, var(--accent) 15%, #ffffff);
}

/* The zebra stops at the recommended column. It used to darken the violet by
   a step on every other row, which broke the one column that is supposed to
   read as a single block into stripes — from a metre away the table looked
   like it had a rendering fault rather than a recommendation. One column,
   one colour, top to bottom. */
.cmp--full tbody tr:nth-child(even) td:nth-child(4) {
  background: var(--accent);
}

/* The row-label column carries a tint of its own so it reads as the spine of
   the table rather than as a sixth plan. It used to sit at --surface, which
   is a 3% wash and disappeared against the white cells beside it; this is the
   same hue with enough of it to see. */
.cmp th:first-child,
.cmp td:first-child {
  text-align: left;
  font-weight: 600;
  color: var(--ink);
  position: sticky;
  left: 0;
  background: color-mix(in srgb, var(--accent) 9%, #ffffff);
  min-width: 190px;
}

.cmp thead th:first-child {
  background: color-mix(in srgb, var(--accent) 15%, #ffffff);
}

/* Only the *first* head row sits in the panel's corner. The price row lives in
   the same <thead> — so the unscoped rule rounded its left cell too, and the
   corner appeared again halfway down the table, where there is no corner. */
.cmp thead tr:first-child th:first-child {
  /* Follows the panel's own corner. Square, it painted over the rounding and
     left a hard notch in the top left of an otherwise drawn frame. The panel
     radius less its 2px border is the radius of the hole it has to fill. */
  border-start-start-radius: calc(var(--r-xl) - 2px);
}

/* Same on the other side, where the last column meets the panel's corner. */
.cmp thead tr:first-child th:last-child {
  border-start-end-radius: calc(var(--r-xl) - 2px);
}

.cmp tbody tr:last-child th,
.cmp tbody tr:last-child td {
  border-bottom: 0;
}

.cmp tbody tr:hover td {
  background: var(--surface);
}

.cmp tbody tr:hover th:first-child {
  background: color-mix(in srgb, var(--accent) 20%, #ffffff);
}

/* Hovering a row must not wash the recommended column out to a tint — that
   was what the pale lavender here did, so running the pointer down the table
   made the column flicker between violet and near-white. It lifts within its
   own colour instead. */
.cmp--full tbody tr:hover td:nth-child(4) {
  background: color-mix(in srgb, var(--accent) 88%, #ffffff);
}

/* The table answers in marks, not sentences — see compare-table.html. That
   only works if the two marks are told apart at a glance and from an angle,
   so the tick is large and saturated and the dash is small and grey. Both
   carry an aria-label; the size difference is for eyes only. */
.cmp .yes {
  color: var(--accent-strong);
  font-weight: 800;
  font-size: 19px;
  line-height: 1;
}

/* "Unavailable" is information, not decoration, so it has to be readable:
   --ink-20 rendered it at 1.46:1. The lighter weight still sets it apart
   from the cells that say a feature is present. */
.cmp .no {
  color: var(--ink-40);
  font-weight: 400;
  font-size: 17px;
  line-height: 1;
}

/* The two rows that are a quantity rather than a yes or a no. ∞ is drawn a
   size up because at the body size it reads as a smudge. */
.cmp .lim {
  font-weight: 800;
  font-size: 18px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.cmp .cell-note {
  display: block;
  font-size: 0.6875rem;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--ink-40);
  margin-top: 2px;
}

/* --------------------------------------------------------------------------
   15. FAQ / accordion — white 12px-radius items, plus icon behaviour.
   -------------------------------------------------------------------------- */

.faq {
  display: grid;
  gap: 14px;
  max-width: 820px;
}

/* Mittig gesetzte Abschnitte ziehen die Karten mit — sonst steht die
   Überschrift in der Mitte und die Liste links darunter. */
.section-head--center + .faq {
  margin-inline: auto;
}

.faq details {
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  transition:
    box-shadow var(--base) var(--ease),
    translate var(--base) var(--ease),
    background var(--base) var(--ease);
}

/* Eine Karte, die man anfassen kann, hebt sich unter dem Zeiger und fällt
   beim Klick in ihren eigenen Schatten — dieselbe Sprache wie die Knöpfe. */
@media (hover: hover) and (pointer: fine) {
  .faq details:hover {
    box-shadow: var(--shadow-2);
    translate: -2px -2px;
  }
}

.faq details[open] {
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
  translate: -1px -1px;
}

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: 22px 24px;
  cursor: pointer;
  list-style: none;
  font-size: clamp(15px, 1.8vw, 17.5px);
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: -0.005em;
  transition: color var(--fast) var(--ease);
}

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

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

.faq summary:focus-visible {
  outline: var(--stroke-w) solid var(--ink);
  outline-offset: -6px;
  border-radius: var(--r-lg);
}

/* Das Plus bekommt eine eigene Fläche: ein Zeichen ohne Feld sieht auf einer
   Karte mit Kontur aus wie ein vergessener Rest. */
.faq summary .chev {
  flex: none;
  box-sizing: content-box;
  width: 16px;
  height: 16px;
  padding: 7px;
  border: var(--stroke);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink);
  transition:
    rotate var(--base) var(--ease),
    background var(--base) var(--ease);
}

.faq details[open] summary .chev {
  rotate: 45deg;
  background: var(--surface-1, #fff);
}

/* Zwischen Frage und Antwort liegt eine Linie, sonst beginnt die Antwort
   mitten im Satz der Frage. */
.faq .answer {
  padding: 20px 24px 24px;
  margin-inline: 24px;
  margin-bottom: 4px;
  border-top: 1px solid color-mix(in srgb, var(--ink) 16%, transparent);
  padding-inline: 0;
  color: var(--ink-70);
  font-weight: 500;
  font-size: clamp(14.5px, 1.6vw, 16px);
  line-height: 1.75;
  max-width: 68ch;
  text-align: left;
}

.faq .answer p + p {
  margin-top: var(--s-3);
}

.faq .answer strong {
  color: var(--ink);
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   16. Roadmap

   Two columns per entry: a narrow rail that says what the thing is called and
   how certain it is, and a wide body that says what it does. The rail is
   right-aligned so both columns read against the same gutter, which is what
   lets the statuses be scanned down the page without reading a word of the
   prose beside them.
   -------------------------------------------------------------------------- */

/* The four stages are the filter. Buttons, not decoration: pressing one
   narrows the table below, pressing it again clears it. */
.rm-filter {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-4);
}

.rm-filter__stage {
  position: relative;
  display: grid;
  justify-items: start;
  gap: 2px;
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  text-align: start;
  transition: box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

.rm-filter__stage:hover {
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.rm-filter__stage:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

/* Pressed reads as pressed: it drops into its own shadow and stays there, so
   the state is a position as well as a colour. */
.rm-filter__stage[aria-pressed="true"] {
  box-shadow: none;
  translate: var(--press) var(--press);
}

.rm-filter__stage[data-state="shipped"] {
  background: var(--pop-mint);
}

.rm-filter__stage[data-state="progress"] {
  background: var(--pop-sun);
}

.rm-filter__stage[data-state="planned"] {
  background: var(--tile-purple);
}

.rm-filter__count {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 4vw, 2.75rem);
  line-height: 1;
  letter-spacing: -0.012em;
}

.rm-filter__name {
  margin-top: var(--s-2);
  font-size: var(--t-sm);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.rm-filter__note {
  color: var(--ink-60);
  font-size: var(--t-xs);
  font-weight: 500;
}

/* The answer block: the three facts stated once, in full sentences, before
   any of the detail. */
.rm-answer {
  padding: var(--s-6) var(--s-7);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

.rm-answer h2 {
  margin-bottom: var(--s-3);
}

.rm-answer p {
  max-width: 78ch;
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-70);
}

/* --------------------------------------------------------------------------
   The roadmap board

   Quarters are columns. "When" is read across and "what" is read down, which
   is the shape of the question people bring to a roadmap; a stack of rows made
   the reader carry the quarter in their head while they scrolled.
   -------------------------------------------------------------------------- */

/* The board is ruled like a table.

   It was five loose stacks of cards with air between them, which read as a
   pinboard: nothing said that the columns were the same kind of thing, or
   that a card belonged to the column above it rather than to the gap. One ink
   frame, one rule under the header row and a rule between every pair of
   columns is all it takes for the eye to read the same content as a table.

   The rules are borders on the columns rather than a background pattern, so
   they land exactly on the column edges at every width and disappear with the
   column when a stage filter empties it. */
.rm-board {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(15.5rem, 1fr);
  align-items: stretch;
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  /* Wider than the viewport is the normal case for five quarters, so it
     scrolls rather than squeezing the cards past legibility. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  /* Two rows for the whole table: the header band and everything under it.
     The columns take these rows as a subgrid, which is what makes the rule
     under the heads one straight line — "Already shipped" needs three lines
     of note and "Q4 2026" needs two, and without this the rule stepped down
     between them. */
  /* Three: the header band, the rows, and the expander at the foot. A column
     with only two rows put its expander on top of its own list, because the
     third child had nowhere else to go. */
  grid-template-rows: auto 1fr auto;
}

.rm-col {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  align-content: start;
  scroll-snap-align: start;
  /* The line between two quarters. The last one has none, and neither has a
     column the filter has hidden, so the table never ends on a stray rule. */
  border-right: 1px solid var(--ink-20);
  transition: background var(--base) var(--ease);
}

.rm-col:last-child,
.rm-col[data-hidden] {
  border-right: 0;
}

/* The quarter under the pointer lifts out of the table very slightly — enough
   to say "this column", not enough to break the grid. */
.rm-col:hover {
  background: var(--accent-06);
}

.rm-col__head {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--s-4) var(--s-5);
  /* The header row is one rule across the whole table, so the heads share a
     bottom border rather than each carrying its own box. */
  border-bottom: var(--stroke);
  background: var(--ink);
  color: var(--ground);
}

/* The quarter being lived through carries the sun, so "now" is findable
   without reading a date. */
.rm-col[data-current] .rm-col__head {
  background: var(--pop-sun);
  color: var(--ink);
}

.rm-col__when {
  font-size: var(--t-sm);
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.rm-col__note {
  margin-top: 2px;
  font-size: var(--t-xs);
  font-weight: 500;
  color: rgba(255, 255, 255, 0.82);
}

.rm-col[data-current] .rm-col__note {
  color: var(--ink-70);
}

.rm-col__list {
  display: grid;
  align-content: start;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A row in the table, not a card on a board: the outline and the offset
   shadow are gone, and what separates two entries is the same hairline that
   separates two columns. */
.rm-card {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-5);
  border-bottom: 1px solid var(--ink-10);
  transition: background var(--fast) var(--ease);
}

.rm-card:last-child {
  border-bottom: 0;
}

/* The row under the pointer takes the ink and a bar down its leading edge —
   the table equivalent of picking a card up. */
.rm-card {
  position: relative;
}

.rm-card::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 3px;
  background: var(--accent);
  scale: 1 0;
  transform-origin: 50% 0;
  transition: scale 260ms var(--ease-out);
}

.rm-card:hover {
  background: var(--surface-2);
}

.rm-card:hover::before {
  scale: 1 1;
}

.rm-card__title {
  font-size: var(--t-sm);
  font-weight: 800;
  letter-spacing: -0.01em;
}

.rm-card__text {
  font-size: var(--t-xs);
  font-weight: 500;
  color: var(--ink-70);
}

.rm-card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-2);
}

.rm-module,
.rm-status {
  display: inline-block;
  padding: 3px 10px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  white-space: nowrap;
}

[data-state="shipped"] .rm-status {
  background: var(--pop-mint);
}

[data-state="progress"] .rm-status {
  background: var(--pop-sun);
}

[data-state="planned"] .rm-status {
  background: var(--tile-purple);
}

/* A filled dot on the one stage that is actually moving. Reinforcement: the
   word beside it says the same thing. */
.rm-card[data-state="progress"] .rm-status::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-inline-end: 5px;
  border-radius: 50%;
  background: var(--ink);
  vertical-align: 1px;
}

[data-rm-row][data-hidden],
[data-rm-row][data-capped],
[data-rm-group][data-hidden] {
  display: none;
}

/* Folds a long column back to a readable height. It only appears on a column
   that is actually holding cards back, and it says how many. */
.rm-more {
  padding: var(--s-3) var(--s-4);
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  font-size: var(--t-xs);
  font-weight: 800;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  transition: box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

.rm-more:hover {
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.rm-more:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes rm-row-in {
    from {
      opacity: 0;
      translate: 0 8px;
      scale: 0.98;
    }
    to {
      opacity: 1;
      translate: 0 0;
      scale: 1;
    }
  }

  [data-rm-row][data-entering] {
    animation: rm-row-in 340ms var(--ease-out) both;
    animation-delay: var(--rm-delay, 0ms);
  }
}

/* --------------------------------------------------------------------------
   Idea cards + the form they open
   -------------------------------------------------------------------------- */

.idea-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  gap: var(--s-5);
}

.idea-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-5);
  align-items: start;
  padding: var(--s-6);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  text-align: start;
  transition: box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

.idea-card:hover {
  box-shadow: var(--shadow-3);
  translate: -2px -2px;
}

.idea-card:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

.idea-card--primary {
  background: var(--tile-purple);
}

.idea-card__art {
  display: block;
  width: 72px;
  flex: none;
}

.idea-card__art svg {
  width: 100%;
  height: auto;
  overflow: visible;
}

.idea-card__body {
  display: grid;
  gap: var(--s-2);
}

.idea-card__title {
  font-size: var(--t-h4);
  font-weight: 800;
  letter-spacing: -0.015em;
}

.idea-card__text {
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-70);
}

.idea-card__cta {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-2);
  font-size: var(--t-sm);
  font-weight: 800;
  color: var(--accent-strong);
}

.idea-card__cta svg {
  transition: translate var(--fast) var(--ease);
}

.idea-card:hover .idea-card__cta svg {
  translate: 2px -2px;
}

/* The plain link under the cards is the route that still works with the
   dialog unavailable. It is small on purpose: it is a fallback, not a
   third option. */
.rm-fallback {
  margin-top: var(--s-5);
  text-align: center;
  font-size: var(--t-xs);
  font-weight: 500;
  color: var(--ink-60);
}

@media (max-width: 680px) {
  .idea-card {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-4);
  }
}

@media (prefers-reduced-motion: reduce) {
  .rm-card,
  .idea-card,
  .idea-card__cta svg {
    transition: none;
  }
}

.rm-note {
  margin-top: var(--s-5);
  max-width: 78ch;
  color: var(--ink-60);
  font-size: var(--t-sm);
  font-weight: 500;
}

@media (max-width: 700px) {
  .rm-filter {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --------------------------------------------------------------------------
   17. CTA band — near-black panel, the design's dark column.
   -------------------------------------------------------------------------- */

/* The closing panel: a white sheet outlined in ink, with the squared
   texture reading through it. Same object language as every card, just at
   full width — which is why it needs no colour of its own. */
.cta {
  position: relative;
  overflow: hidden;
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  color: var(--ink);
  padding: clamp(2.5rem, 6vw, 4.5rem) var(--gutter);
  text-align: center;
  box-shadow: var(--shadow-3);
}

/* Squared paper inside the panel, faded out at the bottom so the buttons
   sit on plain white. */
.cta::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(var(--ink-06) 1px, transparent 1px),
    linear-gradient(90deg, var(--ink-06) 1px, transparent 1px);
  background-size: 32px 32px;
  mask-image: radial-gradient(80% 120% at 50% 0%, #000, transparent 72%);
  pointer-events: none;
}

.cta > * {
  position: relative;
}

.cta .lead,
.cta .muted,
.cta p {
  color: var(--ink-70);
}

.cta .h2,
.cta h2 {
  color: var(--ink);
}

.cta__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3);
  margin-top: var(--s-6);
}

/* --------------------------------------------------------------------------
   18. Footer — canvas ground, uppercase link columns, blue hover.
   -------------------------------------------------------------------------- */

/* The page ends on a deep violet-black panel that the drawn wave cuts
   into — the one place on the site where a section boundary is a shape
   instead of a line, and the one dark surface in a white-mode design.
   Ending on ink is what stops the page rather than letting it fade out.

   The wave is a mask on the block's own colour, so it can never drift
   away from the panel it belongs to and needs no image file.

   `position: relative` and the padding above the content are what give
   the wave room; without them it would cut into the first row of links. */
.footer {
  position: relative;
  border-top: 0;
  background: var(--footer);
  /* Solid white, not a tint of it. On this violet even 90% white measures
     4.3:1, under the floor for body text — so hierarchy here is carried by
     size and weight, never by fading the ink. */
  color: #ffffff;
  /* The top figure was set when the footer was three short columns and the
     wave needed clearing; with five columns and a legal row underneath, that
     much air read as a gap rather than as breathing room. The wave lives
     entirely above the block (`bottom: 100%`), so the only thing this padding
     has to buy is distance from the crest to the first line — and a third
     less of it is still more than the crest dips. */
  padding-block: clamp(3.25rem, 6vw, 4.5rem) var(--s-10);
  margin-top: var(--section-y);
}

.footer::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 100%;
  height: clamp(48px, 6vw, 92px);
  background: var(--footer);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 100' preserveAspectRatio='none'%3E%3Cpath d='M0 44c150-34 292-40 438-18 146 22 262 44 402 40 140-4 300-44 420-56 80-8 140-6 180 4v86H0z' fill='%23000'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 100' preserveAspectRatio='none'%3E%3Cpath d='M0 44c150-34 292-40 438-18 146 22 262 44 402 40 140-4 300-44 420-56 80-8 140-6 180 4v86H0z' fill='%23000'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  pointer-events: none;
}

/* The ink line along the wave crest. It reads against both the white
   above it and the violet below, which an accent-coloured line would not.
   Drawn as a second masked layer rather than a stroke on the same path,
   so the fill stays opaque where the line dips. */
.footer::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 100%;
  height: clamp(48px, 6vw, 92px);
  background: var(--ink);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 100' preserveAspectRatio='none'%3E%3Cpath d='M0 44c150-34 292-40 438-18 146 22 262 44 402 40 140-4 300-44 420-56 80-8 140-6 180 4' fill='none' stroke='%23000' stroke-width='4' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 100' preserveAspectRatio='none'%3E%3Cpath d='M0 44c150-34 292-40 438-18 146 22 262 44 402 40 140-4 300-44 420-56 80-8 140-6 180 4' fill='none' stroke='%23000' stroke-width='4' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  pointer-events: none;
}

.footer__grid {
  display: grid;
  gap: 40px;
  grid-template-columns: 1fr;
  margin-bottom: var(--s-12);
}

@media (min-width: 720px) {
  .footer__grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 1040px) {
  .footer__grid {
    grid-template-columns: 1.6fr repeat(4, 1fr);
  }
}

/* One address, set apart from the blurb above it and the icons below. */
.footer__contact {
  margin-top: var(--s-4);
}

.footer__contact a {
  font-size: 15px;
  font-weight: 700;
  color: #ffffff;
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 2px;
  text-decoration-color: rgba(255, 255, 255, 0.45);
  transition: text-decoration-color var(--fast) var(--ease);
}

.footer__contact a:hover {
  text-decoration-color: currentColor;
}

/* The legal row. Ten pages that have to exist, that almost nobody is looking
   for, and that used to be a column shouting in upper case beside the
   documentation. A row, set smaller, is the right amount of prominence. */
.footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 22px;
  padding-bottom: 28px;
}

.footer__legal a {
  font-size: 13px;
  font-weight: 500;
  color: #ffffff;
  text-decoration-color: transparent;
  text-underline-offset: 4px;
  transition: text-decoration-color var(--fast) var(--ease);
}

.footer__legal a:hover {
  text-decoration: underline;
  text-decoration-color: currentColor;
}

/* The wordmark and the blurb both sit on the dark panel now. */
.footer .brand {
  color: #ffffff;
}

.footer__about .lead {
  max-width: 320px;
  margin-top: var(--s-4);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.6;
  color: #ffffff;
}

.footer__social {
  display: flex;
  gap: 12px;
  margin-top: var(--s-6);
}

.footer__social a {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 2px solid rgba(255, 255, 255, 0.28);
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.06);
  color: #ffffff;
  transition: background var(--fast) var(--ease),
    border-color var(--fast) var(--ease), color var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

/* No hard offset here: an ink shadow is invisible on ink. The fill and
   the border carry the hover instead. */
.footer__social a:hover {
  background: #ffffff;
  border-color: #ffffff;
  color: var(--footer);
  translate: -2px -2px;
}

.footer__social svg {
  width: 18px;
  height: 18px;
}

.footer__title {
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #ffffff;
  margin-bottom: 20px;
}

.footer__links {
  display: grid;
  gap: 14px;
}

/* Sentence case at a normal weight. Set in upper case at 700 the links were
   as loud as the headings above them, so a column of seven read as seven
   headings and the eye had nothing to skim — which is the whole job of a
   footer. Contrast is untouched: this is still solid white. */
.footer__links a {
  font-size: 14px;
  font-weight: 500;
  line-height: 1.35;
  color: #ffffff;
  text-decoration-color: transparent;
  text-underline-offset: 4px;
  transition: text-decoration-color var(--fast) var(--ease);
}

/* The resting state is already full white, so hover cannot be a step up the
   colour scale — it has to be a rule, which also costs nothing in contrast. */
.footer__links a:hover {
  text-decoration: underline;
  text-decoration-color: currentColor;
}

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding-top: 32px;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: #ffffff;
}

/* The status link and its dot live in the footer bottom row. */
.footer__bottom a {
  color: #ffffff;
}

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

.footer :focus-visible {
  outline-color: var(--footer-ink);
}

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

/* The label is uppercase, so it has no descenders. `align-items: center`
   centres against the line box, whose lower half is reserved for descenders
   that never arrive — the dot lands about 0.05em below the optical middle of
   the caps, which at this size is enough to look like a mistake. Measured at
   0.7px on a 13px label; the nudge is in em so it survives a size change. */
.status-pill::before {
  width: 8px;
  height: 8px;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.25);
  translate: 0 -0.05em;
  content: "";
}

.status-pill[data-state="operational"]::before {
  background: var(--pop-mint);
}

.status-pill[data-state="degraded"]::before {
  background: var(--pop-sun);
}

.status-pill[data-state="unavailable"]::before {
  background: var(--pop-coral);
}

/* --------------------------------------------------------------------------
   19. Language switcher
   -------------------------------------------------------------------------- */

.lang {
  position: relative;
}

.lang__button {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border: var(--stroke);
  border-radius: var(--r-sm);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}

.lang__button:hover {
  color: var(--ink);
  border-color: var(--ink);
}

.lang__menu {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + var(--s-2));
  z-index: 120;
  display: none;
  grid-template-columns: repeat(2, minmax(140px, 1fr));
  gap: 2px;
  max-height: 60vh;
  overflow-y: auto;
  padding: var(--s-2);
  background: var(--surface-2);
  border: var(--stroke);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
}

.lang__menu[data-open="true"] {
  display: grid;
}

.lang__menu a {
  padding: var(--s-2) var(--s-3);
  border-radius: 6px;
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-70);
}

.lang__menu a:hover {
  background: var(--surface);
  color: var(--ink);
}

.lang__menu a[aria-current="true"] {
  color: var(--ink);
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   20. Consent banner
   -------------------------------------------------------------------------- */

.consent {
  position: fixed;
  inset-inline: var(--s-4);
  bottom: var(--s-4);
  z-index: 140;
  display: none;
  gap: var(--s-5);
  align-items: center;
  flex-wrap: wrap;
  max-width: var(--container-sm);
  margin-inline: auto;
  padding: var(--s-5);
  background: var(--surface-2);
  border: var(--stroke);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-3);
  font-size: var(--t-sm);
}

.consent[data-open="true"] {
  display: flex;
}

.consent p {
  flex: 1 1 300px;
  color: var(--ink-70);
  font-weight: 500;
}

.consent a {
  color: var(--accent-strong);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Accept and Decline sit side by side and stay side by side: a Decline that
   wraps under the primary button, or is drawn as a link, is a dark pattern
   whichever way it is explained afterwards. */
.consent__actions {
  display: flex;
  flex: 0 0 auto;
  gap: var(--s-3);
  flex-wrap: wrap;
}

.consent__actions .btn {
  flex: 1 1 auto;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   21. Docs layout
   -------------------------------------------------------------------------- */

.docs {
  display: grid;
  gap: var(--s-10);
  align-items: start;
  grid-template-columns: 1fr;
  padding-block: var(--s-12);
}

@media (min-width: 1000px) {
  .docs {
    grid-template-columns: 240px minmax(0, 1fr) 200px;
    gap: var(--s-12);
  }
}

.docs__side {
  position: sticky;
  top: calc(var(--header-h) + var(--s-6));
  max-height: calc(100vh - var(--header-h) - var(--s-12));
  overflow-y: auto;
}

.docs__group + .docs__group {
  margin-top: var(--s-6);
}

.docs__group-title {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: var(--s-3);
}

.docs__side a {
  display: block;
  padding: var(--s-2) var(--s-3);
  border-inline-start: 1px solid var(--line);
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-60);
  transition: color var(--fast) var(--ease),
    border-color var(--fast) var(--ease);
}

.docs__side a:hover {
  color: var(--ink);
}

.docs__side a[aria-current="true"] {
  color: var(--ink);
  font-weight: 700;
  border-inline-start-color: var(--ink);
}

.prose {
  max-width: 72ch;
}

.prose > * + * {
  margin-top: var(--s-4);
}

.prose h2 {
  font-size: var(--t-h3);
  margin-top: var(--s-12);
  padding-top: var(--s-2);
  scroll-margin-top: calc(var(--header-h) + var(--s-6));
}

.prose h3 {
  font-size: var(--t-h4);
  margin-top: var(--s-8);
  scroll-margin-top: calc(var(--header-h) + var(--s-6));
}

.prose h4 {
  font-size: var(--t-lg);
  margin-top: var(--s-6);
}

.prose p,
.prose li {
  color: var(--ink-70);
}

.prose strong {
  color: var(--ink);
  font-weight: 700;
}

.prose a {
  color: var(--accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  text-decoration-color: rgba(8, 121, 204, 0.4);
}

.prose a:hover {
  text-decoration-color: currentColor;
}

.prose ul,
.prose ol {
  display: grid;
  gap: var(--s-2);
  padding-inline-start: var(--s-5);
}

.prose ul li {
  list-style: none;
  position: relative;
}

.prose ul li::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(-1 * var(--s-4));
  top: 0.66em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
}

.prose ol {
  list-style: decimal;
}

.prose ol li::marker {
  color: var(--ink-40);
  font-size: 0.875em;
}

.prose blockquote {
  padding-inline-start: var(--s-5);
  border-inline-start: 1px solid var(--ink-20);
  color: var(--ink-70);
  font-style: italic;
}

.prose code {
  font-family: var(--mono);
  font-size: 0.875em;
  padding: 0.15em 0.4em;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 4px;
  color: var(--ink);
}

.prose pre {
  overflow-x: auto;
  padding: var(--s-5);
  background: var(--surface);
  border: var(--stroke);
  border-radius: var(--r-lg);
}

.prose pre code {
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-90);
}

.prose table {
  font-size: var(--t-sm);
}

.prose th,
.prose td {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--line);
  text-align: start;
}

.prose th {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink);
}

/* Eine Tabelle im Fliesstext darf die Seite nicht breiter machen.

   Bei der Standard-Berechnung (`table-layout: auto`) haengt die Breite einer
   Tabelle am Inhalt: die Spalten fordern so viel Platz, wie ihr laengster
   Text braucht, und diese Forderung wird nach oben durchgereicht, bis das
   ganze Dokument breiter ist als der Bildschirm. Auf einem 390px-Telefon
   liefen dadurch ALLE Absaetze des Artikels rechts aus dem Bild, nicht nur
   die Tabelle — gemessen 25px, und mit einer `min-width` als vermeintlicher
   Abhilfe 170px.

   `table-layout: fixed` loest die Breite vom Inhalt: die Spalten teilen sich,
   was da ist, der Text bricht um, und der Ueberlauf ist null. Nachgemessen,
   nicht geschaetzt. `overflow-x` bleibt als Auffangnetz fuer einzelne
   unteilbare Zeichenketten stehen, etwa eine lange URL in einer Zelle. */
.prose-table {
  overflow-x: auto;
  margin-block: var(--s-6);
}

.prose-table > table {
  width: 100%;
  table-layout: fixed;
}

/* Eine Tabelle mit vielen Spalten rollt seitwaerts, statt sich zu quetschen.

   `width: 0` mit `min-width: 100%` ist der Kern und sieht falscher aus, als
   es ist. Das Problem: eine Tabelle mit Mindestbreite macht ihren Container
   breiter, der Container macht die Spalte breiter, und die Spalte bestimmt
   eigentlich die Breite der Tabelle — eine Schleife, aus der man nur
   herauskommt, indem man dem Container eine Breite gibt, die nichts vom
   Inhalt weiss. `width: 0` tut genau das, `min-width: 100%` fuellt ihn danach
   wieder auf die Breite des Elternteils auf, und `overflow-x` laesst die
   Tabelle darin rollen.

   Der naheliegendere `max-width: calc(100vw - 2rem)` funktionierte fast:
   `100vw` zaehlt die Rollleiste mit, was bei 768px zwei Pixel Ueberlauf
   uebrig liess. Gemessen, nicht vermutet. */
.prose-table--wide {
  width: 0;
  min-width: 100%;
}

.prose-table--wide > table {
  table-layout: auto;
  min-width: 44rem;
}

/* Die erste Spalte traegt meist kurze Schlagworte. Ohne diese Zeile teilte
   `fixed` allen Spalten dieselbe Breite zu und verschenkte den Platz an die
   kuerzeste. */
.prose-table th:first-child,
.prose-table td:first-child {
  width: 26%;
}

/* Auf einem Telefon sind 26 % keine 90 Pixel, und „Integration" ist breiter
   als das. Die Schlagwortspalte bekommt dort mehr Anteil, sonst bricht sie
   mitten im Wort, waehrend rechts daneben Platz frei bleibt. */
@media (max-width: 640px) {
  .prose-table th:first-child,
  .prose-table td:first-child {
    width: 34%;
  }

  /* Das Zellenpolster steht in styles.css auf 1rem links und rechts. Bei
     einer Spalte von 118px sind das 32px Polster fuer 86px Text, und
     „collaboration," passt in 86px nicht mehr. Auf dem Telefon zaehlt jeder
     Pixel Textbreite mehr als die Luft daneben. */
  .prose-table th,
  .prose-table td {
    padding: var(--s-3) var(--s-2);
  }
}

/* Bei fester Spaltenbreite hat ein Wort, das breiter ist als seine Spalte,
   keinen Ausweg mehr: es laeuft in die Nachbarspalte und legt sich ueber
   deren Text. Auf 390px tat das „Unlimited/Business);" — ein Schraegstrich
   ist keine Umbruchstelle.

   `break-word` und NICHT `anywhere`: der Unterschied ist kein Detail.
   `anywhere` zerteilt ein Wort schon am Zeilenende, auch wenn es auf der
   naechsten Zeile ganz hineinpasst — die Spalten lasen sich danach als
   „collaboratio-n" und „dependenci-es". `break-word` schiebt das Wort erst
   auf die naechste Zeile und bricht nur, wenn es auch dort nicht passt. */
.prose-table th,
.prose-table td {
  overflow-wrap: break-word;
}


.prose img {
  border: var(--stroke);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
}

/* Eine Abbildung mit Bildunterschrift. `max-width: 100%` und `height: auto`
   sind hier kein Zierrat: ein Foto bringt seine eigene Pixelbreite mit, und
   ohne diese beiden Zeilen macht ein 1600px breites Bild dasselbe wie zuvor
   die Tabelle — es schiebt die Seite auf, bis der Fliesstext rechts aus dem
   Bild laeuft. */
.post-figure {
  margin-block: var(--s-6);
}

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

.post-figure figcaption {
  margin-top: var(--s-3);
  font-size: var(--t-sm);
  color: var(--ink-70, var(--muted));
}

/* Der Hinweis auf ein maschinell erzeugtes Bild.
 *
 * Artikel 50 der KI-Verordnung verlangt bei Motiven, die echt aussehen, einen
 * Hinweis, den ein Mensch bemerkt. Deshalb steht er als eigene Zeile unter der
 * Bildunterschrift statt als Zusatz am Satzende: angehaengt liest er sich wie
 * ein Teil der Beschreibung und wird ueberlesen.
 *
 * Kleiner und blasser als die Unterschrift, aber mit Rahmen — er soll als
 * Kennzeichnung erkennbar sein und nicht als Fortsetzung des Textes. */
.ai-note {
  display: inline-block;
  margin-top: var(--s-2);
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--r-round);
  font-size: var(--t-xs, 12px);
  line-height: 1.5;
  color: var(--ink-60, var(--muted));
  white-space: nowrap;
}

.post-figure figcaption:empty {
  margin-top: 0;
}

/* Auf der Kachel ist kein Platz fuer den ganzen Satz. Das Kuerzel sitzt neben
   dem Kategorie-Schild, der volle Wortlaut steht im title-Attribut. */
.post-art__ai {
  position: absolute;
  z-index: 3;
  left: 7%;
  top: 7%;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--ink);
  border-radius: var(--r-round);
  background: var(--paper, #fff);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.5px;
  color: var(--ink-70, var(--muted));
}

/* The languages this particular article exists in.
   hreflang in the head talks to search engines; this row talks to readers and
   to any crawler that follows links rather than reading head tags. It wraps
   rather than scrolls: with thirty languages a single line would either
   overflow or hide most of them behind a gesture nobody makes. */
.post-langs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2) var(--s-3);
  margin-top: var(--s-10);
  padding-top: var(--s-5);
  border-top: 1px solid var(--line);
  font-size: var(--t-sm);
}

.post-langs > span {
  color: var(--muted);
  font-weight: 600;
}

.post-langs a {
  color: var(--ink-70, var(--muted));
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.post-langs a:hover,
.post-langs a:focus-visible {
  color: var(--ink);
  border-bottom-color: currentColor;
}

/* Callout — the blue media tint, full panel, no side stripe. */
.callout {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--tile-sky);
}

.callout__label {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--accent-strong);
}

.callout p {
  font-size: var(--t-sm);
  color: var(--ink-70);
}

/* Keyboard key */
kbd {
  display: inline-block;
  padding: 0.1em 0.45em;
  font-family: var(--mono);
  font-size: 0.8125em;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--surface);
  color: var(--ink-90);
  white-space: nowrap;
}

/* Table of contents */
.toc {
  position: sticky;
  top: calc(var(--header-h) + var(--s-6));
  display: none;
  font-size: var(--t-xs);
}

@media (min-width: 1000px) {
  .toc {
    display: block;
  }
}

.toc__title {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: var(--s-3);
}

.toc a {
  display: block;
  padding: var(--s-1) 0;
  color: var(--ink-60);
  font-weight: 500;
}

.toc a:hover,
.toc a[aria-current="true"] {
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   Calculators

   The pages borrow the blog's skeleton — the same hero, section heads, card
   grid and article measure — because they are the same kind of thing to a
   reader: a page you land on from a search, read once, and act on. What is
   left here is only what the blog has no equivalent for.
   -------------------------------------------------------------------------- */

/* Tabular figures. A result that changes as you drag a slider must not also
   change width, or the eye reads movement where there is only arithmetic. */
.tnum {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* The blog's card without its cover: same outline, shadow and hover lift. */
.calc-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  color: var(--ink);
}

.calc-card__title {
  font-size: var(--t-lg);
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.02em;
}

.calc-card__text {
  color: var(--ink-70);
  font-size: var(--t-sm);
  font-weight: 500;
}

/* Pushed to the bottom so the row of cards ends on one line however uneven
   the descriptions are. */
.calc-card__go {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
  padding-top: var(--s-3);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.calc-card__go svg {
  width: 15px;
  height: 15px;
  transition: translate var(--fast) var(--ease);
}

.calc-card:hover .calc-card__go svg {
  translate: 4px 0;
}

/* The formula, stated under the tool rather than hidden in the prose: it is
   the reason to trust the number above it. */
.calc__formula {
  margin-top: var(--s-5);
  color: var(--ink-70);
  font-size: var(--t-sm);
  font-weight: 500;
}

.calc-cta {
  margin-top: var(--s-10);
}

@media (prefers-reduced-motion: reduce) {
  .calc-card__go svg {
    transition: none;
  }

  .calc-card:hover .calc-card__go svg {
    translate: 0 0;
  }
}

/* --------------------------------------------------------------------------
   22. Blog
   -------------------------------------------------------------------------- */

.post-grid {
  display: grid;
  gap: 24px;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
}

.blog-feature {
  display: grid;
  grid-template-columns: minmax(0, 1.12fr) minmax(20rem, 0.88fr);
  min-height: 28rem;
  margin-bottom: var(--s-10);
  overflow: hidden;
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
}

.blog-feature__art {
  min-height: 24rem;
  border-right: 1px solid var(--line);
}

.blog-feature__art .post-card__cover {
  height: 100%;
  border-bottom: 0;
}

.blog-feature__copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(2rem, 5vw, 4rem);
}

.blog-feature__copy h2 {
  margin-top: var(--s-5);
  font-size: clamp(2rem, 1.4rem + 2vw, 3.25rem);
  line-height: 1.05;
  letter-spacing: -0.03em;
}

.blog-feature__copy > p {
  margin-top: var(--s-5);
  color: var(--ink-70);
  font-size: var(--t-lg);
  font-weight: 500;
}

.blog-feature__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  margin-top: var(--s-8);
  color: var(--ink-60);
  font-size: var(--t-sm);
}

.blog-section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-6);
  margin: var(--s-12) 0 var(--s-6);
  scroll-margin-top: calc(var(--header-h) + var(--s-6));
}

.blog-section-head p {
  margin-top: var(--s-2);
  color: var(--ink-60);
  font-weight: 500;
}

.blog-section-head > a {
  color: var(--ink);
  font-weight: 800;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.blog-section-head > a:hover {
  color: var(--accent-strong);
}

.post-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}

/* The cover is a drawn panel with its own ink edge against the card body,
   and it leans in slightly when the card is hovered — the only motion on
   a blog index, so it reads as "this one" rather than as decoration. */
.post-card__cover {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--tile-purple);
  border-bottom: var(--stroke);
  object-fit: cover;
  width: 100%;
}

/* Ein Foto als Titelbild statt der gezeichneten Kachel.

   `object-fit: cover` auf dem Bild und nicht auf dem Kasten: die Regel
   darueber steht auf einem <div>, wo sie nichts tut — sie wirkt nur auf
   ersetzte Elemente. Das Seitenverhaeltnis der Fotos (16:9) passt genau,
   trotzdem beschneidet `cover` sauber, falls ein spaeteres Bild anders
   proportioniert ist, statt es zu verzerren. */
.post-card__cover--photo {
  background: var(--ink-10, #eee);
}

.post-card__cover--photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Das Kategorie-Schild lag auf einer hellen Zeichnung. Auf einem Foto braucht
   es einen eigenen Grund, sonst verschwindet es je nach Bildausschnitt. */
.post-card__cover--photo .post-art__label {
  background: var(--bg);
  border: var(--stroke);
}

.post-card__cover > * {
  transition: scale var(--slow) var(--ease-out);
}

.post-card:hover .post-card__cover > * {
  scale: 1.04;
}

@media (prefers-reduced-motion: reduce) {
  .post-card:hover .post-card__cover > * {
    scale: 1;
  }
}

.post-card__cover--guides {
  background: var(--tile-sun);
}

.post-card__cover--engineering {
  background: var(--tile-sky);
}

.post-art__window {
  position: absolute;
  inset: 12% 9%;
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
}

.post-art__window::after {
  position: absolute;
  inset: 22% 8% 10%;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  content: "";
}

.post-art__window i {
  position: relative;
  z-index: 1;
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  margin: 0.8rem 0 0 0.35rem;
  border-radius: 50%;
  background: var(--pop-coral);
}

.post-art__window i:first-child {
  margin-left: 0.8rem;
  background: var(--pop-sun);
}

.post-art__window i:last-child {
  background: var(--pop-mint);
}

.post-art__shape {
  position: absolute;
  z-index: 2;
  border: var(--stroke);
}

.post-art__shape--one {
  right: 16%;
  bottom: 18%;
  width: 25%;
  aspect-ratio: 1;
  rotate: 8deg;
  border-radius: var(--r-sm);
  background: var(--pop-purple);
}

.post-art__shape--two {
  left: 18%;
  bottom: 22%;
  width: 34%;
  height: 16%;
  rotate: -5deg;
  border-radius: var(--r-round);
  background: var(--pop-mint);
}

.post-art__label {
  position: absolute;
  z-index: 3;
  right: 7%;
  top: 7%;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--ink);
  border-radius: var(--r-round);
  background: var(--surface-2);
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.post-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  flex: 1;
}

.post-card__meta {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.post-card__title {
  font-size: var(--t-h4);
  line-height: 1.25;
}

.post-card__excerpt {
  font-size: var(--t-sm);
  color: var(--ink-60);
  font-weight: 500;
}

.post-header {
  padding-block: var(--s-12) var(--s-8);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s-10);
}

.post-header__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  margin-top: var(--s-6);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.blog-newsroom-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-6);
  margin-top: var(--s-12);
  padding: var(--s-6);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.blog-newsroom-link p {
  max-width: 64ch;
  margin-top: var(--s-1);
  color: var(--ink-70);
  font-weight: 500;
}

@media (max-width: 820px) {
  .blog-feature {
    grid-template-columns: 1fr;
  }

  .blog-feature__art {
    min-height: 18rem;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }

  .blog-section-head,
  .blog-newsroom-link {
    align-items: flex-start;
    flex-direction: column;
  }
}

/* --------------------------------------------------------------------------
   23. Press and newsroom
   -------------------------------------------------------------------------- */

.press-assets {
  display: grid;
  gap: var(--s-3);
  margin-top: var(--s-5);
}

.list {
  display: grid;
  gap: var(--s-2);
  margin-top: var(--s-5);
  padding-inline-start: var(--s-5);
}

.list li {
  position: relative;
}

.list li::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(-1 * var(--s-4));
  top: 0.7em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent-strong);
}

.press-assets li {
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--line);
  color: var(--ink-70);
}

.press-assets a,
.press-facts a {
  color: var(--accent-strong);
  font-weight: 700;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent-strong) 45%, transparent);
  text-underline-offset: 3px;
}

.press-assets a:hover,
.press-facts a:hover {
  text-decoration-color: currentColor;
}

.press-facts {
  margin-top: var(--s-5);
  border-top: 1px solid var(--line);
}

.press-facts > div {
  display: grid;
  grid-template-columns: minmax(8rem, 0.35fr) minmax(0, 1fr);
  gap: var(--s-5);
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--line);
}

.press-facts dt {
  font-weight: 800;
}

.press-facts dd {
  color: var(--ink-70);
}

@media (max-width: 560px) {
  .press-facts > div {
    grid-template-columns: 1fr;
    gap: var(--s-1);
  }
}

.newsroom-hero {
  padding: clamp(4rem, 9vw, 8rem) 0;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.newsroom-hero > .wrap {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(18rem, 0.55fr);
  gap: clamp(2rem, 7vw, 7rem);
  align-items: end;
}

.newsroom-hero__copy .h1 {
  max-width: 14ch;
}

.newsroom-hero__copy .lede {
  max-width: 62ch;
  margin-top: var(--s-6);
}

.newsroom-stamp {
  display: inline-block;
  margin-bottom: var(--s-5);
  padding: 6px 16px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-60);
  box-shadow: var(--shadow-1);
}

.newsroom-brief {
  padding: var(--s-6);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  rotate: 0deg;
}

.newsroom-brief > span {
  display: block;
  color: var(--ink-60);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.newsroom-brief > strong {
  display: block;
  margin-top: var(--s-4);
  font-size: var(--t-h4);
}

.newsroom-brief p {
  margin-top: var(--s-2);
  color: var(--ink-70);
  font-weight: 500;
}

.newsroom-brief a {
  display: inline-block;
  margin-top: var(--s-5);
  color: var(--ink);
  font-weight: 800;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.newsroom-brief a:hover {
  color: var(--accent-strong);
}

.newsroom-heading {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-8);
  margin-bottom: var(--s-8);
}

.newsroom-heading p {
  max-width: 62ch;
  margin-top: var(--s-2);
  color: var(--ink-60);
  font-weight: 500;
}

.newsroom-list {
  border-top: 1px solid var(--line);
}

.news-item {
  display: grid;
  grid-template-columns: 12rem minmax(0, 1fr);
  border-bottom: 1px solid var(--line);
}

.news-item__date {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-8) var(--s-6) var(--s-8) 0;
  border-right: 1px solid var(--line);
  font-size: var(--t-sm);
}

.news-item__date time {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-60);
}

.news-item__date span {
  align-self: flex-start;
  padding: 0.25rem 0.55rem;
  border: 1px solid rgba(255, 153, 0, 0.3);
  border-radius: 4px;
  background: var(--tile-sun);
  color: var(--pop-coral);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.news-item__body {
  position: relative;
  padding: var(--s-8) clamp(1.5rem, 5vw, 4rem);
}

.news-item__number {
  position: absolute;
  right: 0;
  top: var(--s-6);
  color: var(--ink-10);
  font-size: clamp(2.5rem, 6vw, 5rem);
  font-weight: 800;
  line-height: 1;
}

.news-item__body h3 {
  max-width: 24ch;
  padding-right: 5rem;
  font-size: var(--t-h3);
}

.news-item__body p {
  max-width: 72ch;
  margin-top: var(--s-3);
  color: var(--ink-70);
  font-weight: 500;
}

.news-item__body a {
  display: inline-block;
  margin-top: var(--s-5);
  color: var(--ink);
  font-weight: 800;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.news-item__body a:hover {
  color: var(--accent-strong);
}

/* Three fixed fractions had no way to stop being three: on a 390px phone the
   third column started 40px past the right edge and took the page with it.
   auto-fit still gives three across on a wide screen and stacks them when
   there is no room, which is what the fractions were trying to express. */
.newsroom-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: clamp(2rem, 4vw, 3.5rem);
}

  .newsroom-heading {
    align-items: flex-start;
    flex-direction: column;
  }

  .news-item {
    grid-template-columns: 1fr;
  }

  .news-item__date {
    flex-direction: row;
    align-items: center;
    padding: var(--s-5) 0;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }

  .news-item__body {
    padding-inline: 0;
  }
}

.read-progress {
  position: fixed;
  inset-inline: 0;
  top: var(--header-h);
  height: 2px;
  z-index: 90;
  background: var(--accent);
  transform-origin: 0 50%;
  scale: 0 1;
}

/* --------------------------------------------------------------------------
   24. Live status
   -------------------------------------------------------------------------- */

.status-page {
  min-height: 70vh;
}

.status-hero {
  padding: clamp(3rem, 7vw, 6rem) 0 var(--s-8);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

/* The heading and the button share one line, and the button stays on the
   right of it. It used to be allowed to wrap, which on this narrow container
   dropped it under the heading on most laptops — where it reads as the next
   thing to do rather than as the way to report what you are looking at. */
.status-hero__top {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-8);
}

/* A flex item will not shrink below its content unless it is told it may.
   Without this the long lede propped the row open and forced the wrap this
   rule is here to prevent. */
.status-hero__top > div:first-child {
  min-width: 0;
}

@media (max-width: 640px) {
  .status-hero__top {
    flex-wrap: wrap;
    align-items: flex-start;
  }
}

/* "Report a problem" is a label, not a paragraph: broken over two lines it
   reads as two separate instructions. It keeps one line at every width — the
   row above wraps and drops it onto its own line long before the button
   itself would have to fold. */
.status-hero__top > .btn {
  flex: none;
  white-space: nowrap;
  text-wrap: nowrap;
}

.status-hero__top .lead {
  max-width: 62ch;
  margin-top: var(--s-4);
}

.status-kicker {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-bottom: var(--s-4);
  padding: 6px 16px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
  box-shadow: var(--shadow-1);
}

.status-kicker::before {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--ink);
  background: var(--pop-mint);
  content: "";
}

.status-tabs {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: var(--s-10);
  padding: 4px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.status-tabs button {
  min-height: 2.75rem;
  padding: 12px 24px;
  border: 0;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.status-tabs button:hover {
  color: var(--ink);
}

.status-tabs button[aria-current="true"] {
  background: var(--ink);
  color: var(--ground);
}

/* Both status segment rows sit in a pill track with 4px of padding. A
   segment at either end has to pick that curve up on its outer side: a 6px
   corner nested inside a 100px one is invisible while the segment is
   transparent and reads as a mistake the moment it fills with ink. The
   inner corners stay small, so the row still reads as one control split
   into segments rather than three loose buttons. Logical properties, so
   the first segment rounds on the correct side in the RTL locales too. */
.status-tabs button:first-child,
.status-range button:first-child {
  border-start-start-radius: var(--r-round);
  border-end-start-radius: var(--r-round);
}

.status-tabs button:last-child,
.status-range button:last-child {
  border-start-end-radius: var(--r-round);
  border-end-end-radius: var(--r-round);
}

.status-summary {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-5);
  margin-bottom: var(--s-8);
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.status-summary__icon {
  display: grid;
  width: 3.25rem;
  height: 3.25rem;
  place-items: center;
  border: var(--stroke);
  border-radius: 50%;
  background: var(--tile-purple);
  font-size: 1.5rem;
  font-weight: 800;
}

.status-summary h2 {
  font-size: var(--t-h4);
  line-height: 1.2;
}

.status-summary p {
  color: var(--ink-60);
  font-weight: 500;
}

.status-summary__time {
  max-width: 20ch;
  text-align: right;
  font-size: var(--t-xs);
}

.status-summary[data-state="operational"] .status-summary__icon {
  background: var(--tile-mint);
}

.status-summary[data-state="degraded"] .status-summary__icon {
  background: var(--tile-sun);
}

.status-summary[data-state="unavailable"] .status-summary__icon {
  background: var(--tile-coral);
}

.status-service-list,
.status-event-list {
  display: grid;
  gap: var(--s-5);
}

.status-service {
  padding: var(--s-6);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.status-service__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-6);
}

.status-service__identity {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
}

.status-dot {
  flex: 0 0 auto;
  width: 0.8rem;
  height: 0.8rem;
  margin-top: 0.35rem;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background: var(--pop-mint);
}

.status-service[data-state="degraded"] .status-dot {
  background: var(--pop-sun);
}

.status-service[data-state="unavailable"] .status-dot {
  background: var(--pop-coral);
}

.status-service__name {
  font-size: 1.05rem;
}

.status-service__description {
  max-width: 62ch;
  margin-top: var(--s-1);
  color: var(--ink-60);
  font-size: var(--t-sm);
  font-weight: 500;
}

.status-service__metric {
  flex: 0 0 auto;
  text-align: right;
  font-size: var(--t-xs);
}

.status-service__metric strong,
.status-service__metric span {
  display: block;
}

.status-service__metric span {
  color: var(--ink-60);
}

.status-bars {
  display: flex;
  align-items: stretch;
  gap: 3px;
  min-height: 2rem;
  margin-top: var(--s-5);
  overflow: hidden;
}

.status-bar {
  flex: 1 1 8px;
  min-width: 3px;
  max-width: 12px;
  border: 1px solid var(--ink);
  border-radius: 3px;
  background: var(--pop-mint);
}

.status-bar[data-state="degraded"] {
  background: var(--pop-sun);
}

.status-bar[data-state="unavailable"] {
  background: var(--pop-coral);
}

.status-bars__empty {
  display: grid;
  width: 100%;
  min-height: 2rem;
  place-items: center;
  border: 1px dashed var(--ink-20);
  border-radius: var(--r-sm);
  color: var(--ink-60);
  font-size: var(--t-xs);
}

.status-service__details {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
  margin-top: var(--s-3);
  color: var(--ink-60);
  font-size: var(--t-xs);
}

.status-history-note,
.status-privacy {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  margin-top: var(--s-6);
  padding: var(--s-4);
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--tile-sun);
  color: var(--ink-70);
  font-size: var(--t-sm);
}

.status-history-note strong {
  flex: 0 0 auto;
  color: var(--ink);
}

.status-panel-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-6);
  margin-bottom: var(--s-8);
}

.status-panel-head p {
  max-width: 60ch;
  margin-top: var(--s-2);
  color: var(--ink-60);
  font-weight: 500;
}

.status-loading,
.status-empty,
.status-event {
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
}

.status-loading {
  padding: var(--s-10);
  text-align: center;
  color: var(--ink-60);
}

.status-empty {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 9rem;
  padding: var(--s-6);
}

.status-empty__icon {
  display: grid;
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  place-items: center;
  border: var(--stroke);
  border-radius: 50%;
  background: var(--tile-mint);
  font-weight: 800;
}

.status-empty p {
  margin-top: var(--s-1);
  color: var(--ink-60);
}

.status-event {
  padding: var(--s-6);
}

.status-event__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  margin-bottom: var(--s-3);
}

.status-event__badge {
  flex: 0 0 auto;
  padding: 4px 10px;
  border: 1px solid var(--ink);
  border-radius: var(--r-round);
  background: var(--tile-purple);
  color: var(--ink);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.status-event__time {
  margin-top: var(--s-4);
  color: var(--ink-60);
  font-size: var(--t-xs);
}

.status-update {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr) auto;
  gap: var(--s-4);
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
}

.status-update strong {
  text-transform: capitalize;
}

.status-update time {
  color: var(--ink-60);
  font-size: var(--t-xs);
}

.status-explainer {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(16rem, 0.75fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}

.status-explainer > div > p {
  max-width: 68ch;
  margin-top: var(--s-4);
}

.status-privacy {
  margin-top: 0;
  background: var(--tile-mint);
}

.status-privacy > span {
  font-size: 1.5rem;
  font-weight: 800;
}

@media (max-width: 760px) {
  .status-hero__top,
  .status-service__top,
  .status-panel-head {
    align-items: stretch;
    flex-direction: column;
  }

  .status-tabs {
    display: flex;
  }

  .status-tabs button {
    flex: 1 1 10rem;
  }

  .status-summary {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .status-summary__time {
    grid-column: 1 / -1;
    max-width: none;
    text-align: left;
  }

  .status-service__metric {
    text-align: left;
  }

  .status-history-note,
  .status-explainer {
    display: grid;
    grid-template-columns: 1fr;
  }

  .status-update {
    grid-template-columns: 1fr;
    gap: var(--s-1);
  }
}

/* --------------------------------------------------------------------------
   24b. Status metrics + past incidents

   The response-time charts render only from measured daily averages the
   /api/status/history endpoint returns — no data, no line. The range
   toggle reuses the segmented-control language of the page tabs.
   -------------------------------------------------------------------------- */

.status-metrics {
  margin-top: var(--s-12);
}

.status-range {
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.status-range button {
  padding: 8px 16px;
  border: 0;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease);
}

.status-range button:hover {
  color: var(--ink);
}

.status-range button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--ground);
}

.status-metric-list {
  display: grid;
  gap: var(--s-5);
}

.status-metric {
  padding: var(--s-6);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

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

.status-metric__name {
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.status-metric__value {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--accent-strong);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.status-metric__chart {
  width: 100%;
  height: auto;
  display: block;
}

.status-metric__chart .spark-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.status-metric__chart .spark-area {
  fill: var(--accent-10);
  stroke: none;
}

.status-metric__chart .spark-axis {
  stroke: var(--ink-20);
  stroke-width: 1;
}

.status-metric__chart .spark-label {
  font-size: 10px;
  font-weight: 600;
  fill: var(--ink-60);
}

.status-metric__empty {
  display: grid;
  min-height: 4rem;
  place-items: center;
  border: 1px dashed var(--ink-20);
  border-radius: var(--r-sm);
  color: var(--ink-60);
  font-size: var(--t-xs);
}

.status-metric__foot {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  margin-top: var(--s-2);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-60);
}

/* Past incidents — a day-by-day ledger for the last week. */
.status-past {
  margin-top: var(--s-12);
}

.status-past__list {
  display: grid;
  gap: var(--s-6);
  margin-top: var(--s-6);
}

.status-past__day {
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
}

.status-past__date {
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-bottom: var(--s-2);
}

.status-past__none {
  color: var(--ink-60);
  font-size: var(--t-sm);
  font-weight: 500;
}

.status-past__incident {
  margin-top: var(--s-2);
}

.status-past__title {
  font-size: var(--t-sm);
  font-weight: 700;
  color: var(--pop-coral);
}

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

.status-past__update strong {
  color: var(--ink);
  font-weight: 700;
  /* The API stores states lowercase; the ledger reads as a label. */
  text-transform: capitalize;
}

.status-past__time {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-60);
  margin-top: 2px;
}

/* --------------------------------------------------------------------------
   25. Reveal-on-scroll — the Webflow .reveal behaviour: 20px rise, 0.6s
   ease, staggered per group by app.js. Gated on .reveal-ready with a
   failsafe so content can never stay hidden.
   -------------------------------------------------------------------------- */

.reveal-ready [data-reveal] {
  opacity: 0;
  translate: 0 20px;
  transition: opacity var(--slow) var(--ease), translate var(--slow) var(--ease),
    scale var(--slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, translate;
}

.reveal-ready [data-reveal="scale"] {
  translate: 0 20px;
  scale: 0.985;
}

.reveal-ready [data-reveal="fade"] {
  translate: 0 0;
}

/* The alternating feature rows come in from the side they sit on, so the
   copy and its illustration meet in the middle as the row arrives. The
   travel is kept modest — a long slide reads as a slow page, not a lively
   one — and it collapses to a plain rise on narrow screens, where the two
   halves are stacked and a sideways entrance means nothing. */
.reveal-ready [data-reveal="left"] {
  translate: -34px 0;
}

.reveal-ready [data-reveal="right"] {
  translate: 34px 0;
}

@media (max-width: 860px) {
  .reveal-ready [data-reveal="left"],
  .reveal-ready [data-reveal="right"] {
    translate: 0 20px;
  }
}

.reveal-ready [data-reveal].is-visible {
  opacity: 1;
  translate: 0 0;
  scale: 1;
}

/* --------------------------------------------------------------------------
   26. Responsive
   -------------------------------------------------------------------------- */

@media (max-width: 980px) {
  .nav,
  .header__actions .btn--ghost {
    display: none;
  }

  .burger {
    display: flex;
    margin-inline-start: auto;
  }

  .header__actions {
    margin-inline-start: var(--s-3);
  }
}

@media (max-width: 620px) {
  :root {
    --header-h: 60px;
  }

  /* The wave keeps its seven bows at every width, so on a phone they are
     squeezed into a fifth of the room and the same amplitude turns them
     from curves into a zigzag. Shortening the box flattens them back —
     `preserveAspectRatio="none"` scales the path vertically with it. The
     deeper desktop swing needs a shorter box here than the old one did. */
  .header::before,
  .header::after {
    height: 8px;
  }

  .brand {
    font-size: 23px;
  }

  .header__inner {
    gap: var(--s-3);
  }

  .header__actions {
    gap: var(--s-2);
    margin-inline-start: auto;
  }

  .header__actions > .btn--primary {
    display: none;
  }

  .hero__actions .btn {
    width: 100%;
  }

  .lang__menu {
    grid-template-columns: 1fr;
  }

  .footer__bottom {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* --------------------------------------------------------------------------
   27. Reduced motion and print
   -------------------------------------------------------------------------- */

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

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

  [data-reveal] {
    opacity: 1;
    translate: 0 0;
    scale: 1;
  }

  .marquee__track {
    animation: none;
  }
}

@media print {
  .header,
  .footer,
  .consent,
  .canvas-bg,
  .marquee,
  .read-progress {
    display: none !important;
  }

  body {
    background: #fff;
    color: #000;
  }
}

/* --------------------------------------------------------------------------
   28. Forms
   -------------------------------------------------------------------------- */

.support-form {
  display: grid;
  gap: var(--s-5);
  padding: clamp(24px, 3vw, 32px);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

/* Der Kleine auf der Kante des Formulars — ein Kreis mit Gesicht, mehr nicht.
   Er sitzt rechts oben, wo die Karte ihre gerade Strecke hat; auf der Rundung
   selbst laege er schief. `bottom: calc(100% - 5px)` laesst ihn fuenf Pixel
   ueber die Kante fallen; genau diese Ueberlappung ist der Unterschied
   zwischen sitzen und schweben, und sie traegt nur, weil die Zeichnung unten
   am Kreis endet. Der Wert haengt an der Groesse: bei 46px steht der Schatten
   knapp fuenf Pixel unter dem Kreis, und beide Zahlen muessen zusammen
   wandern. Platz darueber schafft `margin-top`, sonst raeumte die Figur sich
   in die Zeile darueber. */
/* Ein weiterführender Verweis unter einem Raster. Die Section-Heads bringen
   ihren eigenen Abstand mit, ein Raster nicht — ohne diese Zeile klebt der
   Verweis an der untersten Karte. */
.section-more {
  margin-top: var(--s-10);
}

.form-perch {
  position: relative;
  margin-top: clamp(26px, 3vw, 34px);
}

.form-perch__pal {
  position: absolute;
  right: clamp(20px, 5vw, 56px);
  bottom: calc(100% - 5px);
  z-index: 1;
}

.field {
  position: relative;
  display: grid;
  gap: var(--s-2);
}

/* The label follows the field into focus by colour alone. There used to be a
   violet rail down the left as well; it drew a second, competing edge next to
   a box that already has an ink outline, and on a stack of fields it read as
   a margin note rather than as focus. The box itself says which field is
   live — that is enough, and it is what a form is expected to look like. */
.field label {
  font-size: var(--t-base);
  font-weight: 700;
  transition: color 200ms var(--ease);
}

.field:focus-within label {
  color: var(--accent-strong);
}

/* Once a field holds something, its label steps back: the eye should land on
   what is still empty. */
.field:has(:is(input, textarea):not(:placeholder-shown):not(:focus)) label {
  color: var(--ink-70);
}

.field-hint {
  max-width: 70ch;
  margin: 0;
  color: var(--ink-70);
  font-size: var(--t-xs);
  line-height: 1.5;
}

.field input,
.field select,
.field textarea {
  width: 100%;
  /* Bigger than the 12px/13px this started at. A form is the one place on the
     site where someone types, and a control the size of a caption asks them
     to aim. 16px is also the size below which iOS zooms the page on focus,
     which is its own kind of "cut off". */
  min-height: 52px;
  padding: 14px 16px;
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--t-base);
  box-shadow: 0 0 0 0 var(--accent-20);
  transition: border-color var(--fast) var(--ease),
    box-shadow 220ms var(--ease-out), translate 200ms var(--ease-out),
    background-color var(--fast) var(--ease);
}

.field textarea {
  resize: vertical;
  min-height: 9rem;
  line-height: 1.6;
}

.field input:hover,
.field select:hover,
.field textarea:hover {
  border-color: var(--ink-20);
}

/* --------------------------------------------------------------------------
   Form select — a drawn list instead of the operating system's

   A <select> takes styling right up to the moment it opens, and then the
   platform draws the list: system blue, system corners, system font. app.js
   keeps the real control for the form to submit and builds this over it.
   -------------------------------------------------------------------------- */

.select-x {
  position: relative;
}

/* Still in the form and still submitting, just not on screen. Not
   display:none — a hidden-by-display control is skipped by some autofill and
   validation paths, and this one is the field. */
.select-x__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  pointer-events: none;
}

.select-x__combo {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  width: 100%;
  min-height: 52px;
  padding: 14px 16px;
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--t-base);
  cursor: pointer;
  user-select: none;
  box-shadow: 0 0 0 0 var(--accent-20);
  transition: border-color var(--fast) var(--ease),
    box-shadow 220ms var(--ease-out), translate 200ms var(--ease-out);
}

.select-x__combo:hover {
  border-color: var(--ink-20);
}

/* The same ring the text fields take on focus, so a form of mixed controls
   still has one focus language. */
.select-x__combo:focus-visible {
  outline: none;
  border-color: var(--accent-strong);
  box-shadow: 0 0 0 4px var(--accent-20), 3px 3px 0 var(--accent-strong);
  translate: -1px -1px;
}

.select-x__chevron {
  flex: none;
  transition: rotate var(--fast) var(--ease);
}

.select-x__combo[aria-expanded="true"] .select-x__chevron {
  rotate: 180deg;
}

/* Fixed, and placed by app.js from the control's own box: the list has to
   escape the form, and inside the pricing dialog it has to sit in the dialog
   rather than on the body, which the top layer would render it beneath. */
.select-x__list {
  position: fixed;
  z-index: 200;
  max-height: min(320px, 50vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  padding: var(--s-2);
  list-style: none;
  background: var(--surface-2);
  border: var(--stroke);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
  transform-origin: top center;
  animation: select-drop 200ms var(--ease-out) both;
}

@keyframes select-drop {
  from {
    opacity: 0;
    translate: 0 -8px;
    scale: 0.97;
  }
}

.select-x__option {
  padding: var(--s-2) var(--s-3);
  border-radius: 8px;
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-70);
  cursor: pointer;
  transition: background var(--fast) var(--ease),
    color var(--fast) var(--ease), translate var(--fast) var(--ease);
}

/* Pointer and keyboard land on the same highlight — the pointer sets the
   active option too, so there is never a second, competing one. */
.select-x__option.is-active {
  background: var(--tile-purple);
  color: var(--ink);
  translate: 2px 0;
}

.select-x__option[aria-selected="true"] {
  color: var(--ink);
  font-weight: 700;
}

@media (prefers-reduced-motion: reduce) {
  .select-x__chevron {
    transition: none;
  }

  .select-x__list {
    animation: none;
  }

  .select-x__option,
  .select-x__combo,
  .field input,
  .field select,
  .field textarea,
  .field label {
    transition: none;
  }

  .select-x__option.is-active,
  .select-x__combo:focus-visible,
  .field input:focus-visible,
  .field select:focus-visible,
  .field textarea:focus-visible {
    translate: 0 0;
  }
}

.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: none;
  border-color: var(--accent-strong);
  /* The ring and the ink offset together: the field lifts off the sheet the
     way every other object on this site does when it becomes the live one. */
  box-shadow: 0 0 0 4px var(--accent-20), 3px 3px 0 var(--accent-strong);
  translate: -1px -1px;
}

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

/* The sentence on the left, the one button on the right, where a form ends
   and where the eye is already heading after the last field. */
/* The send button sits at the end of the row, where a form's action belongs
   in every reading direction — `flex-end` rather than `right`, so it follows
   the language rather than the screen. */
.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-4);
  margin-top: var(--s-5);
}

.form-actions > p {
  flex: 1 1 260px;
  margin: 0;
}

.form-actions .btn--primary {
  min-width: 168px;
}

.form-actions .btn {
  flex: none;
  min-height: 44px;
}

.support-fallback {
  white-space: normal;
}

/* The challenge draws nothing unless a visitor actually has to prove
   something, so it reserves nothing either. It used to hold four rems of
   height for a green tick that told nobody anything. */
.support-challenge {
  min-height: 0;
}

.support-challenge:empty {
  display: none;
}

.support-challenge:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* What stands where the form stood, once the message is away.

   The old confirmation was a sentence under a form that was still sitting
   there, fields and all, asking the reader to work out whether anything had
   happened. The form has no further job at that point, so it goes: a mark, a
   word, one line, and the single control that ends the errand. */
.form-done {
  display: grid;
  justify-items: center;
  gap: var(--s-3);
  padding: var(--s-7) var(--s-5) var(--s-6);
  text-align: center;
  animation: form-done-in 0.42s var(--ease-out) both;
}

.form-done__mark {
  width: 84px;
  height: 84px;
  /* The tick draws itself once, on arrival. A loop would turn a moment of
     relief into a thing that keeps asking to be looked at. */
  animation: form-done-mark 0.5s var(--ease-out) 0.08s both;
}

.form-done__mark path {
  stroke-dasharray: 62;
  stroke-dashoffset: 62;
  animation: form-done-tick 0.42s var(--ease-out) 0.28s both;
}

.form-done__title {
  margin: 0;
  /* It takes focus so the reader is moved to it, not because anyone is
     meant to tab here; a ring around a word nobody can act on reads as a
     control that does nothing. */
  outline: none;
  font-size: clamp(1.375rem, 1.15rem + 0.9vw, 1.75rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.form-done__lead {
  margin: 0;
  max-width: 44ch;
  color: var(--ink-70);
}

.form-done__ref {
  margin: 0;
  padding: 8px 14px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--warm);
  font-size: var(--t-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* The single control sits in the middle here, not at the right edge: there is
   no form left for it to end, and centred under the sentence it reads as the
   way out rather than as one more step. */
.form-done .form-actions {
  justify-content: center;
  margin-top: var(--s-2);
}

@keyframes form-done-in {
  from {
    opacity: 0;
    translate: 0 10px;
  }
}

@keyframes form-done-mark {
  from {
    opacity: 0;
    scale: 0.7;
  }
  70% {
    scale: 1.06;
  }
}

@keyframes form-done-tick {
  to {
    stroke-dashoffset: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .form-done,
  .form-done__mark,
  .form-done__mark path {
    animation: none;
  }

  .form-done__mark path {
    stroke-dashoffset: 0;
  }
}

/* What the form says back.

   It was a line of coloured text, which is what a browser does by default and
   what every other form on the internet looks like. It is a drawn box now,
   with the same ink outline and offset shadow as everything else on the page,
   and an icon so the kind of message is legible before the words are. It only
   appears when there is something to say — an empty reserved line above the
   button was a permanent gap waiting for bad news. */
.form-status {
  margin: 0;
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-70);
}

.form-status[data-state]:not([data-state="idle"]):not(:empty) {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 14px 16px;
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  line-height: 1.45;
  animation: form-status-in 320ms var(--ease-out) both;
}

/* The icon is a pseudo-element, so no state has to add markup to the page. */
.form-status[data-state]:not([data-state="idle"]):not(:empty)::before {
  flex: none;
  width: 22px;
  height: 22px;
  border: 2px solid currentColor;
  border-radius: var(--r-round);
  font-size: 14px;
  font-weight: 800;
  line-height: 18px;
  text-align: center;
  content: "!";
}

@keyframes form-status-in {
  from {
    opacity: 0;
    translate: 0 -6px;
  }
}

.form-status[data-state="queued"],
.form-status[data-state="sending"] {
  border-color: var(--accent-strong);
  background: var(--accent-06);
  color: var(--accent-strong);
}

.form-status[data-state="queued"]:not([data-state="idle"]):not(:empty)::before,
.form-status[data-state="sending"]:not([data-state="idle"]):not(:empty)::before {
  content: "→";
}

.form-status[data-state="sent"] {
  border-color: var(--success-ink, #176b3a);
  background: #f1faf4;
  color: var(--success-ink, #176b3a);
}

.form-status[data-state="sent"]:not([data-state="idle"]):not(:empty)::before {
  content: "✓";
}

.form-status[data-state="verify"],
.form-status[data-state="invalid"],
.form-status[data-state="too-large"],
.form-status[data-state="rate"],
.form-status[data-state="timeout"],
.form-status[data-state="rejected"],
.form-status[data-state="unavailable"] {
  border-color: var(--danger-ink, #9f241f);
  background: #fff5f4;
  color: var(--danger-ink, #9f241f);
}

@media (prefers-reduced-motion: reduce) {
  .form-status[data-state]:not([data-state="idle"]):not(:empty) {
    animation: none;
  }
}

.form-status:focus {
  outline: none;
}

.support-form button[aria-busy="true"] {
  cursor: wait;
}

/* Creem trust badge — sits under the plan grid, where a purchase starts. */
.creem-badge {
  margin-top: var(--s-5);
  text-align: center;
}

/* ── Calculators ───────────────────────────────────────────────────────────
   Zwei Spalten: links stellt man ein, rechts steht die Antwort. Auf schmalen
   Geraeten wandert die Antwort nach oben.
   ------------------------------------------------------------------------ */

.calc {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: var(--s-6);
  align-items: start;
  padding: clamp(24px, 3vw, 32px);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.calc__inputs { display: flex; flex-direction: column; gap: var(--s-5); }
.calc__field { display: flex; flex-direction: column; gap: 8px; }
.calc__label { font-size: var(--fs-sm); font-weight: 700; }
.calc__control { display: flex; flex-direction: column; gap: 10px; }

.calc__number {
  font: inherit;
  font-variant-numeric: tabular-nums;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  transition: border-color .18s ease, box-shadow .18s ease;
}
.calc__number:hover { border-color: var(--ink-20); }
.calc__number:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(13, 153, 255, 0.2);
}

/* The track is drawn 4px thin, but a 4px-tall input is a 4px-tall hit area —
   under the 24px WCAG 2.2 asks for and close to impossible with a thumb on a
   phone. The element is now 24px tall with the visible track painted as a
   centred gradient, so the look is unchanged and the whole height is
   grabbable. */
.calc__slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 24px;
  border-radius: 999px;
  /* Two tracks on one element: the violet one is clipped to --fill, which
     app.js writes as a percentage, and the grey one is what is left. A filled
     track says where the value sits inside its range without a second
     element to keep in sync. */
  background:
    linear-gradient(var(--accent) 0 0) no-repeat left center /
      var(--fill, 0%) 4px,
    linear-gradient(var(--ink-10) 0 0) no-repeat center / 100% 4px;
  cursor: pointer;
}
.calc__slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--ink);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
  transition: transform .16s cubic-bezier(.22, 1, .36, 1);
}
.calc__slider:hover::-webkit-slider-thumb { transform: scale(1.12); }
.calc__slider:active::-webkit-slider-thumb { transform: scale(.96); }
.calc__slider::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--ink);
}

/* Not sticky. It used to follow the scroll, which meant that on every screen
   short enough to matter the answer panel slid down over the rows of numbers
   printed underneath it and hid the very figures it was summarising. It sits
   where it is written now, in the drawn frame the rest of the site uses. */
.calc__result {
  padding: var(--s-5);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--accent) 7%, #ffffff);
  border: var(--stroke);
  box-shadow: var(--shadow-1);
}
.calc__result-label {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--ink-60);
}
/* The answer is the one violet thing on the page, and it moves when it
   changes: a number that swaps silently is easy to miss when you are dragging
   a slider and watching your hand. */
/* The answer, and it has to fit whatever the answer turns out to be.

   The size was a fixed clamp, which is correct for "$360" and runs off the
   edge of the panel at "$1,018,500" — seven digits and two separators at
   3.5rem is wider than the column it sits in. `cqi` sizes it against the
   panel rather than the viewport, and the container query below steps it down
   again once the string itself gets long. Whatever is left over wraps rather
   than overflowing: a number on two lines is readable, a number with its last
   digits cut off is not. */
.calc__result {
  container-type: inline-size;
}

.calc__result-value {
  font-size: clamp(1.75rem, 13cqi, 3.5rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -1px;
  color: var(--accent-strong);
  margin: 6px 0 var(--s-4);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
  max-width: 100%;
}

@media (prefers-reduced-motion: no-preference) {
  .calc__result-value.is-updated {
    animation: calc-swap 420ms var(--ease-out);
  }

  @keyframes calc-swap {
    0% {
      opacity: 0.55;
      translate: 0 6px;
      scale: 0.985;
    }
    100% {
      opacity: 1;
      translate: 0 0;
      scale: 1;
    }
  }
}
.calc__rows { display: flex; flex-direction: column; gap: 0; }
.calc__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-3);
  padding: 9px 0;
  font-size: var(--fs-sm);
  color: var(--ink-70);
  border-top: 1px solid var(--line);
}
.calc__row strong { color: var(--ink); font-variant-numeric: tabular-nums; }

/* ── Live-Ticker ───────────────────────────────────────────────────────── */
.calc__ticker {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  margin-top: var(--s-4);
  padding: var(--s-4);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.calc__ticker-value {
  font-size: var(--fs-xl);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.calc__ticker.is-running { border-color: var(--accent); }
.calc__ticker.is-running .calc__ticker-value { color: var(--accent-strong); }
.calc__ticker.is-running .calc__result-label::after {
  content: "";
  display: inline-block;
  width: 7px; height: 7px;
  margin-left: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: calc-pulse 1.4s ease-in-out infinite;
}
@keyframes calc-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .25; }
}

/* ── Diagramme ─────────────────────────────────────────────────────────── */
.calc__chart {
  grid-column: 1 / -1;
  margin: var(--s-5) 0 0;
  padding-top: var(--s-5);
  border-top: 1px solid var(--line);
}
.calc__chart svg { width: 100%; height: auto; overflow: visible; }

.chart__label { font-size: 12px; fill: var(--ink-60); }
.chart__value { font-size: 13px; font-weight: 700; fill: var(--ink); }
.chart__bar { fill: var(--ink-10); }
.chart__bar.is-accent { fill: var(--accent); }

.chart__arc { stroke: var(--ink-10); }
.chart__arc--0 { stroke: var(--accent); }
.chart__arc--1 { stroke: var(--pop-purple); }
.chart__arc--2 { stroke: var(--ink-40); }
.chart__arc--3 { stroke: var(--ink); }

.chart__legend {
  list-style: none;
  margin: var(--s-3) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  font-size: var(--fs-sm);
  color: var(--ink-70);
}
.chart__legend li { display: flex; align-items: center; gap: 7px; }
.chart__swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--ink-10); }
.chart__swatch--0 { background: var(--accent); }
.chart__swatch--1 { background: var(--pop-purple); }
.chart__swatch--2 { background: var(--ink-40); }
.chart__swatch--3 { background: var(--ink); }

.chart__line { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linejoin: round; }
.chart__area { fill: rgba(13, 153, 255, 0.12); stroke: none; }
.chart__dot { fill: var(--accent); stroke: var(--surface-2); stroke-width: 3; }
.chart__zero { stroke: var(--ink-40); stroke-width: 1; stroke-dasharray: 4 4; }

@media (max-width: 860px) {
  .calc { grid-template-columns: 1fr; }
  .calc__result { position: static; order: -1; }
  .calc__ticker { flex-direction: column; align-items: stretch; }
}

@media (prefers-reduced-motion: reduce) {
  .calc__slider::-webkit-slider-thumb,
  .calc__number { transition: none; }
  .calc__ticker.is-running .calc__result-label::after { animation: none; }
}

/* Direktantwort — der Satz, den eine Antwortmaschine zitieren kann. Volle
   Tint-Flaeche statt Seitenstreifen. */
.t-answer-box {
  margin-top: var(--s-5);
  padding: var(--s-4) var(--s-5);
  border: var(--stroke);
  background: var(--tile-sky);
  border-radius: var(--r-md);
}
.t-answer-label {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--accent-strong);
  margin-bottom: 4px;
}
.t-answer { font-size: var(--fs-md); line-height: 1.55; }

/* Ein Knopf, der (noch) nichts tut, muss danach aussehen. */
.btn.is-disabled {
  opacity: .55;
  cursor: not-allowed;
  pointer-events: none;
  background: var(--surface);
  color: var(--ink-60);
  border-color: var(--line);
  box-shadow: none;
}

/* ==========================================================================
   Partner programme

   Five ranks, each drawn as a crest. The crest is one SVG reused five
   times; the rank supplies three colours through custom properties, so a
   sixth rank is three lines of CSS rather than another drawing.

   The metals are flat with faceted highlights rather than gradients: a
   gradient would be the one soft object in a drawn system. Facets read as
   polished without breaking the rule.
   ========================================================================== */

.ranks {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 232px), 1fr));
}

.rank {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-6) var(--s-5) var(--s-5);
  text-align: center;
}

/* The colours each crest paints with. Overridden per rank below. */
.rank__crest {
  --crest-face: var(--ink-20);
  --crest-light: var(--surface-2);
  --crest-deep: var(--ink-40);
  width: clamp(96px, 12vw, 124px);
  height: auto;
  display: block;
  transition: rotate var(--base) var(--ease), scale var(--base) var(--ease-out);
}

/* A crest tips like a shield being turned to catch the light. */
.rank:hover .rank__crest {
  rotate: -3deg;
  scale: 1.04;
}

.rank--bronze {
  background: #fdf3ea;
}
.rank--bronze .rank__crest {
  --crest-face: #c08552;
  --crest-light: #eab887;
  --crest-deep: #8c5a30;
}

.rank--silver {
  background: #f3f5f8;
}
.rank--silver .rank__crest {
  --crest-face: #aab3c0;
  --crest-light: #e2e8f0;
  --crest-deep: #78828f;
}

.rank--gold {
  background: #fff6dd;
}
.rank--gold .rank__crest {
  --crest-face: #e5b02e;
  --crest-light: #ffdd85;
  --crest-deep: #a97b12;
}

.rank--diamond {
  background: #e6f8fc;
}
.rank--diamond .rank__crest {
  --crest-face: #63c9de;
  --crest-light: #c4f0f8;
  --crest-deep: #2d8ba1;
}

/* The top rank is the brand violet, so the last crest is the only one
   that belongs to Ceyu rather than to a metal. */
.rank--crystal {
  background: var(--tile-purple);
}
.rank--crystal .rank__crest {
  --crest-face: var(--accent);
  --crest-light: #cbabff;
  --crest-deep: #5a2ba0;
}

.rank__name {
  font-size: var(--t-h4);
  font-weight: 800;
  letter-spacing: -0.02em;
}

/* The condition that unlocks the rank — the one line people actually
   scan, so it gets the ink and the tabular figures. */
.rank__gate {
  font-size: var(--t-sm);
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.rank__perks {
  display: grid;
  gap: var(--s-2);
  margin-top: var(--s-2);
  padding-top: var(--s-4);
  border-top: 1px solid var(--ink-20);
  width: 100%;
  text-align: start;
}

.rank__perks li {
  position: relative;
  padding-inline-start: var(--s-6);
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-70);
}

.rank__perks li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.42em;
  width: 12px;
  height: 7px;
  border-inline-start: 2.5px solid var(--ink);
  border-bottom: 2.5px solid var(--ink);
  rotate: -45deg;
}

/* --------------------------------------------------------------------------
   The two embeddable badges

   Shown at their real size on a checked ground, because a partner is about
   to paste them onto a site whose background nobody here controls.
   -------------------------------------------------------------------------- */

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

.badge-demo {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-6);
}

.badge-demo__stage {
  display: grid;
  place-items: center;
  padding: var(--s-8) var(--s-4);
  border: 1px solid var(--ink-20);
  border-radius: var(--r-md);
  /* Checkerboard, the universal "this is transparent behind" — it shows
     the badge has its own solid ground and does not rely on the page. */
  background-color: var(--surface-2);
  background-image:
    linear-gradient(45deg, var(--ink-06) 25%, transparent 25%),
    linear-gradient(-45deg, var(--ink-06) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--ink-06) 75%),
    linear-gradient(-45deg, transparent 75%, var(--ink-06) 75%);
  background-size: 18px 18px;
  background-position: 0 0, 0 9px, 9px -9px, -9px 0;
}

/* The badge itself. Written with the same inline-style discipline as the
   Creem badge further down the pricing page: a partner pastes this into
   their own stylesheet-free HTML, so it may not rely on our tokens. */
.partner-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 15px;
  border: 2px solid #151617;
  border-radius: 100px;
  background: #ffffff;
  box-shadow: 2px 2px 0 0 #151617;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  color: #151617;
  text-decoration: none;
}

.partner-badge--premium {
  background: #8d4edc;
  color: #ffffff;
  border-color: #151617;
}

.badge-demo__code {
  position: relative;
  max-height: 11rem;
  overflow: auto;
  margin: 0;
  padding: var(--s-4);
  border: 1px solid var(--ink-20);
  border-radius: var(--r-md);
  background: var(--surface);
  font-size: 11px;
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--ink-70);
}

/* --------------------------------------------------------------------------
   How it runs — a numbered ladder rather than four more cards
   -------------------------------------------------------------------------- */

.ladder {
  display: grid;
  gap: var(--s-6);
  counter-reset: rung;
  max-width: 62ch;
}

.ladder li {
  position: relative;
  counter-increment: rung;
  padding-inline-start: calc(var(--s-12) + var(--s-2));
  min-height: var(--s-10);
}

.ladder li::before {
  content: counter(rung);
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  display: grid;
  place-items: center;
  width: var(--s-10);
  height: var(--s-10);
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  font-size: var(--t-sm);
  font-weight: 800;
}

/* The line that joins the rungs, stopping at the last one. */
.ladder li:not(:last-child)::after {
  content: "";
  position: absolute;
  inset-inline-start: calc(var(--s-5) - 1px);
  top: var(--s-10);
  bottom: calc(-1 * var(--s-6));
  width: 2px;
  background: var(--ink-20);
}

.ladder h3 {
  font-size: var(--t-h4);
  font-weight: 700;
  margin-bottom: var(--s-2);
}

.ladder p {
  color: var(--ink-70);
  font-weight: 500;
}

@media (prefers-reduced-motion: reduce) {
  .rank:hover .rank__crest {
    rotate: 0deg;
    scale: 1;
  }
}

/* ==========================================================================
   Comparison table — the version that has to sell

   Three changes over a plain feature grid, each one earning its place:

     1. The price sits in the table and the buy button sits at the bottom
        of it. Someone who has read fifteen rows to make up their mind
        should not have to scroll back up to act on it.
     2. The recommended column is marked all the way down rather than only
        in the header, because a recommendation that vanishes after the
        second row is not a recommendation.
     3. Below 780px the table becomes a two-column comparison — Basic
        against one plan you pick — because five columns on a phone is a
        table nobody reads.

   What is deliberately not here: countdown timers, seat counters, "N
   people are viewing this". Under EU Directive 2024/825, in force since
   27 March 2026, artificial urgency and invented scarcity are prohibited
   commercial practices, and this shop sells from Germany.
   ========================================================================== */

.cmp--full {
  min-width: 900px;
}

/* The recommended column is the only coloured thing in the table, marked
   from the header down to the button. The tint sits on every cell rather
   than on a <col>, because a column element cannot paint a background
   over a sticky first column.

   Full accent, not the pale tile tint it used to carry. The plan card for the
   same plan is solid violet; a wash three steps lighter over here read as a
   different, weaker recommendation rather than the same one. Everything in
   the column is restated white below — ink on this violet is 3.67:1 and fails
   for text of any size. */
.cmp--full thead th.is-featured,
.cmp--full tbody td:nth-child(4),
.cmp--full tfoot td.is-featured {
  background: var(--accent);
  color: #ffffff;
}

/* Hierarchy inside the column moves to size and weight: on a fill this
   saturated there is no usable band between white and too-grey-to-read. */
.cmp--full tbody td:nth-child(4) .cell-note,
.cmp--full .cmp-row--price td.is-featured .cmp-price__per,
.cmp--full tbody td:nth-child(4).yes,
.cmp--full tbody td:nth-child(4).lim,
.cmp--full tbody td:nth-child(4).no {
  color: #ffffff;
}

/* The ticks stay white. Mint was tried, to give the column a second colour
   cue — it measures 2.1:1 on this violet, and a tick is not decoration: it is
   the cell's entire content, so it has to clear the text floor like any other
   character. The column is already its own cue. */

.cmp--full thead th.is-featured {
  position: relative;
  border-inline: 2px solid var(--ink);
}

.cmp--full tbody td:nth-child(4),
.cmp--full .cmp-row--price td.is-featured {
  border-inline: 2px solid var(--ink);
}

/* The column closes with an ink edge instead of running off the bottom. */
.cmp--full tfoot td.is-featured {
  border-inline: 2px solid var(--ink);
  border-bottom: 2px solid var(--ink);
}

/* Sits with its top edge on the column's top line and hangs into the header
   cell's own padding. Where the panel has room to stop clipping — see the
   wide breakpoint below — it rises to straddle that line the way the badge on
   the recommended plan card sits on its border. */
/* Straddles the table's top rule: its middle sits on the black line, which
   is where a sticker belongs — inside the header cell it read as a second
   heading. The height is fixed and the offset is half of it, so the mark
   lands on whole pixels; centring it with a 50% translate put its text on
   half a device pixel and made it look smudged next to the drawn outline.
   Positioned relative to the panel rather than the cell — see .table-scroll
   below — so the same rule works at every width. */
.cmp-flag {
  --cmp-flag-h: 26px;

  position: absolute;
  top: calc(var(--cmp-flag-h) / -2);
  inset-inline: 0;
  width: max-content;
  margin-inline: auto;
  height: var(--cmp-flag-h);
  display: grid;
  place-items: center;
  padding-inline: 16px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--pop-sun);
  color: var(--ink);
  box-shadow: var(--shadow-1);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  white-space: nowrap;
  z-index: 3;
}

/* While the panel is still a scroller it clips, so the flag has to come back
   inside the header cell or it would be sliced in half by the frame. */
@media (max-width: 1239px) {
  .cmp-flag {
    top: 6px;
  }
}

/* The header row needs headroom for the badge to sit on its edge. */
.cmp--full thead th {
  padding-top: var(--s-8);
  vertical-align: bottom;
}

/* No padding above the table: the recommended column has to reach the panel's
   own top line, and a reserved strip left a white band across the top of it.
   The badge straddles that line instead.

   It cannot bleed out of a scroller — CSS forces the clipped axis to `hidden`
   as soon as the other one scrolls, so overflow-clip-margin has nothing to
   act on. The answer is not to be a scroller at the sizes where nothing needs
   to scroll: the table asks for 900px, so above that it fits and the panel
   can let the badge through. Below it, the badge is clipped again, and below
   780px the table has already collapsed to the two-column phone layout where
   there is no recommended column to flag. */
@media (min-width: 1240px) {
  .table-scroll:has(.cmp--full) {
    overflow: visible;
  }

  /* With the panel no longer clipping, the table's own corners have to follow
     the frame or they square off inside it. */
  .cmp--full tfoot tr:last-child td:last-child {
    border-end-end-radius: calc(var(--r-xl) - 2px);
  }

  .cmp--full tfoot tr:last-child th:first-child,
  .cmp--full tbody tr:last-child th:first-child {
    border-end-start-radius: calc(var(--r-xl) - 2px);
  }
}

.cmp-row--price th,
.cmp-row--price td {
  padding-block: var(--s-4);
  border-bottom: 2px solid var(--ink);
  background: var(--surface-2);
}

.cmp-row--price td.is-featured {
  background: var(--accent);
  color: #ffffff;
}

.cmp-price {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  white-space: nowrap;
}

.cmp-price__amount {
  font-size: clamp(1.25rem, 1rem + 1vw, 1.75rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.cmp-price__per {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-60);
}

/* The buy row. Sticky to the bottom of the scroller so the buttons stay
   reachable while the visitor is still reading rows — the whole point of
   putting them in the table. */
.cmp-row--cta td {
  padding-block: var(--s-5);
  border-bottom: 0;
  vertical-align: top;
}

/* The note under the comparison table. Centred and given the full measure:
   left-aligned at 74ch it sat in the left third of a 1400px table and read as
   a stray caption rather than as a footnote to the whole thing. */
.cmp-note {
  max-width: 100%;
  margin-top: var(--s-6);
  margin-inline: auto;
  text-align: center;
  text-wrap: balance;
  color: var(--ink-60);
  font-weight: 500;
}

/* ------------------------------------------------------- Phone layout -- */

.cmp-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
}

.cmp-picker[hidden] {
  display: none;
}

.cmp-picker__label {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.cmp-picker__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.cmp-picker__row button {
  padding: 9px var(--s-4);
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink);
  transition: background var(--fast) var(--ease),
    color var(--fast) var(--ease), box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

.cmp-picker__row button:hover {
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.cmp-picker__row button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--ground);
}

@media (max-width: 780px) {
  /* Two columns only: the free plan as the baseline, and whichever plan
     the visitor picked. app.js sets data-cmp-show; without it every
     column stays visible and the table simply scrolls, which is the
     correct no-JavaScript outcome. */
  .cmp--full {
    min-width: 0;
  }

  /* `:nth-child(n+3)` rather than a pair of `:not()`s, so this rule and
     the one below it carry the same specificity and source order decides
     — with two `:not()`s the hide rule outranked the show rule and the
     column the visitor had just picked stayed hidden. */
  .cmp--full[data-cmp-show] th:nth-child(n + 3),
  .cmp--full[data-cmp-show] td:nth-child(n + 3) {
    display: none;
  }

  .cmp--full[data-cmp-show="2"] th:nth-child(3),
  .cmp--full[data-cmp-show="2"] td:nth-child(3),
  .cmp--full[data-cmp-show="3"] th:nth-child(4),
  .cmp--full[data-cmp-show="3"] td:nth-child(4),
  .cmp--full[data-cmp-show="4"] th:nth-child(5),
  .cmp--full[data-cmp-show="4"] td:nth-child(5),
  .cmp--full[data-cmp-show="5"] th:nth-child(6),
  .cmp--full[data-cmp-show="5"] td:nth-child(6) {
    display: table-cell;
  }

  .cmp--full th,
  .cmp--full td {
    padding: var(--s-3) var(--s-3);
    font-size: var(--t-xs);
  }

  .cmp--full th:first-child,
  .cmp--full td:first-child {
    min-width: 0;
    width: 50%;
  }

  .cmp-flag {
    font-size: 9px;
    padding: 3px 9px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cmp-picker__row button:hover {
    translate: 0 0;
  }
}

/* ==========================================================================
   Plan card motion

   Three moments, each tied to something that actually happened: the card
   arriving, the visitor flipping the billing switch, and the recommended
   card announcing itself. Nothing loops, nothing repeats on scroll-back,
   and every one of them is short enough to be felt rather than watched.
   A pricing table is where people are deciding — motion here has to
   confirm, never entertain.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  /* 1 — the ticks draw themselves as the card arrives, in reading order.
     Gated on .is-visible, which only the reveal observer sets, so a page
     without JavaScript shows finished ticks rather than empty ones. */
  @keyframes tick-draw {
    from { stroke-dashoffset: 22; }
    to { stroke-dashoffset: 0; }
  }

  .reveal-ready .plan.is-visible .plan__features li svg path {
    stroke-dasharray: 22;
    animation: tick-draw 420ms var(--ease-out) both;
  }

  /* Staggered by position rather than by a JS-written delay: the list is
     static, so the cascade can be static too. Capped at the sixth item —
     beyond that the last tick would arrive after the visitor has read
     past it. */
  .reveal-ready .plan.is-visible .plan__features li:nth-child(1) svg path { animation-delay: 140ms; }
  .reveal-ready .plan.is-visible .plan__features li:nth-child(2) svg path { animation-delay: 210ms; }
  .reveal-ready .plan.is-visible .plan__features li:nth-child(3) svg path { animation-delay: 280ms; }
  .reveal-ready .plan.is-visible .plan__features li:nth-child(4) svg path { animation-delay: 350ms; }
  .reveal-ready .plan.is-visible .plan__features li:nth-child(5) svg path { animation-delay: 420ms; }
  .reveal-ready .plan.is-visible .plan__features li:nth-child(n + 6) svg path { animation-delay: 480ms; }

  /* 2 — the price answers the billing switch. It drops in from above,
     which reads as the new figure replacing the old rather than the old
     one being edited. app.js adds .is-swapped only on a real change. */
  @keyframes price-swap {
    0% { opacity: 0; translate: 0 -10px; }
    60% { opacity: 1; }
    100% { opacity: 1; translate: 0 0; }
  }

  .plan__amount.is-swapped,
  .cmp-price__amount.is-swapped,
  .plan__was.is-swapped {
    animation: price-swap 280ms var(--ease-out);
  }

  /* The strikethrough price arrives a beat later, so the eye lands on the
     figure being charged first and the comparison second. */
  .plan__was.is-swapped {
    animation-delay: 60ms;
    animation-fill-mode: both;
  }

  /* 3 — the recommended card's sticker settles into place once, with the
     smallest possible overshoot. Any more and it becomes a toy. */
  @keyframes badge-settle {
    0% { opacity: 0; translate: 0 8px; scale: 0.86; }
    70% { opacity: 1; scale: 1.04; }
    100% { opacity: 1; translate: 0 0; scale: 1; }
  }

  .reveal-ready .plan--featured.is-visible .plan__badge {
    animation: badge-settle 460ms var(--ease-out) 260ms both;
  }

  /* The comparison table's badge does the same, keyed to its own row. The
     end state has to match the static rule exactly: this animation fills
     `both`, so whatever it lands on wins over the stylesheet afterwards. */
  @keyframes flag-settle {
    0% { opacity: 0; translate: 0 30%; scale: 0.9; }
    100% { opacity: 1; translate: 0 0; scale: 1; }
  }

  .reveal-ready .table-scroll.is-visible .cmp-flag {
    animation: flag-settle 420ms var(--ease-out) 200ms both;
  }

  /* 4 — the recommended card lifts a hair above its neighbours when the
     row is hovered anywhere. It is the only persistent state here, and it
     costs nothing to read: the card you were already being pointed at
     stays the one standing forward. */
  .plans:hover .plan--featured:not(:hover) {
    translate: 0 -2px;
  }
}

/* --------------------------------------------------------------------------
   Modal dialog

   Used by the status page's report form. <dialog> carries the focus trap,
   the inert background and Escape; everything here is presentation.
   -------------------------------------------------------------------------- */

.modal {
  width: min(820px, calc(100vw - 2 * var(--gutter)));
  max-height: min(92vh, 1040px);
  /* A modal dialog is centred by the browser's own `margin: auto`, which the
     `* { margin: 0 }` reset at the top of this file was quietly beating — both
     dialogs on the site opened against the top left corner. Restated here
     rather than exempting dialog from the reset, so the reset stays one rule
     with no exceptions to remember. */
  margin: auto;
  padding: 0;
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-3);
  color: var(--ink);
  overflow: clip;
}

.modal::backdrop {
  background: rgba(21, 22, 23, 0.55);
  backdrop-filter: blur(3px);
}

/* Opening. `allow-discrete` is what lets the dialog animate from display:none
   at all; @starting-style carries the first frame. Browsers without either
   simply get the finished state, which is the resting dialog. */
@starting-style {
  .modal[open] {
    opacity: 0;
  }

  .modal[open] .modal__panel {
    translate: 0 18px;
    scale: 0.965;
  }

  .modal[open]::backdrop {
    opacity: 0;
  }
}

/* The dialog fades; the panel inside it is what rises and grows.

   The movement used to be on the dialog itself, and a translate or a scale on
   an element makes it the containing block for every fixed-position
   descendant — which is how the drawn select list came to be measured against
   the viewport and painted against the dialog, cut off at its edge. Moving the
   two properties one level in costs nothing visually and gives the list its
   viewport back. */
.modal[open] {
  opacity: 1;
  transition: opacity 240ms var(--ease-out), overlay 380ms allow-discrete,
    display 380ms allow-discrete;
}

.modal[open] .modal__panel {
  translate: 0 0;
  scale: 1;
  transition: translate 380ms var(--ease-out), scale 380ms var(--ease-out);
}

.modal[open]::backdrop {
  opacity: 1;
  transition: opacity 260ms var(--ease-out), overlay 260ms allow-discrete,
    display 260ms allow-discrete;
}

/* The panel scrolls, not the dialog: the heading and the close button stay
   put while a long form moves under them. */
.modal__panel {
  position: relative;
  /* A dialog inherits from wherever it sits in the markup, and the pricing
     one sits inside a centred section — which was quietly centring every
     label in the form. A sheet sets its own alignment. */
  text-align: start;
  max-height: inherit;
  padding: 0 var(--s-6) var(--s-8);
  overflow-y: auto;
  /* Anything scrolled into view inside the sheet — a field the browser jumps
     to on a validation failure, the receipt after a send — stops below the
     sticky head instead of behind it. */
  scroll-padding-top: 92px;
  scrollbar-width: thin;
  scrollbar-color: var(--ink-20) transparent;
  overscroll-behavior: contain;
}

.modal__panel::-webkit-scrollbar {
  width: 10px;
}

.modal__panel::-webkit-scrollbar-thumb {
  border: 3px solid transparent;
  border-radius: var(--r-round);
  background: var(--ink-20);
  background-clip: content-box;
}

.modal__panel::-webkit-scrollbar-thumb:hover {
  background: var(--ink-40);
  background-clip: content-box;
}

/* Sticky, so a long form never scrolls its own title and its own way out
   off screen. The rule underneath appears only once something has moved
   beneath it — a line under a heading that is still flush with the top of
   the sheet reads as a seam, not as structure. */
/* The head is violet rather than white: it is the one band that says which
   sheet this is, and a white strip over a white sheet said nothing. It runs
   the full width of the dialog — the negative inline margin cancels the
   panel's padding — and the figure sits on its top edge. */
.modal__head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  margin-inline: calc(-1 * var(--s-6));
  padding: var(--s-6) var(--s-6) var(--s-5);
  background: var(--accent);
  color: #ffffff;
  border-bottom: var(--stroke);
  box-shadow: 0 1px 0 transparent;
  transition: box-shadow 200ms var(--ease);
}

.modal__head .h3,
.modal__head .modal__title {
  color: #ffffff;
}

/* The close control has to survive the violet: an ink outline on a white
   plate, which is the same button the rest of the site uses. */
.modal__head .modal__close {
  background: var(--surface-2);
  color: var(--ink);
}

.modal[data-scrolled] .modal__head {
  box-shadow: 0 1px 0 var(--ink-10), 0 10px 18px -18px var(--ink);
}

/* The reason the panel gets its own soft edge: without it a form taller than
   the sheet simply stops at the border, which reads as a screenshot someone
   cropped. The fade says "there is more", and it is taken away the moment the
   reader reaches the end. */
.modal__fade {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  z-index: 3;
  display: grid;
  justify-items: center;
  align-content: end;
  height: 92px;
  padding-bottom: 10px;
  background: linear-gradient(
    to bottom,
    rgb(255 255 255 / 0) 0%,
    rgb(255 255 255 / 0.72) 42%,
    rgb(255 255 255 / 0.97) 100%
  );
  pointer-events: none;
  opacity: 1;
  transition: opacity 220ms var(--ease);
}

.modal[data-at-end] .modal__fade {
  opacity: 0;
}

/* A drawn chevron riding the fade, bobbing the way the rest of the site's
   marks do. It is the affordance; the gradient alone is easy to miss. */
.modal__fade span {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.modal__close {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  color: var(--ink);
  box-shadow: var(--shadow-1);
  transition: background var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease), translate var(--fast) var(--ease),
    rotate 200ms var(--ease);
}

.modal__close:hover {
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
  rotate: 90deg;
}

.modal__close:active {
  translate: var(--press);
  box-shadow: none;
  rotate: 90deg;
}

.modal__title {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.modal__lead {
  margin-top: var(--s-4);
  margin-bottom: var(--s-5);
  color: var(--ink-60);
}

/* Inside the sheet the form loses its own frame. Two ink outlines nested one
   inside the other is the single cheapest-looking thing a dialog can do. */
.modal .support-form {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}

@media (prefers-reduced-motion: reduce) {
  .modal__close:hover,
  .modal__close:active {
    translate: 0 0;
    rotate: none;
  }

  .modal__fade span {
    animation: none;
  }

  .modal[open],
  .modal[open] .modal__panel,
  .modal[open]::backdrop {
    transition: none;
  }
}

/* ==========================================================================
   AI page

   Structure borrowed from how the category does it — claim, the split, three
   concrete scenarios, the full catalogue, governance — but the substance is
   this product's own: twelve priced actions and a free local summariser, not
   a chat window and a fleet of agents, so the page says that instead.

   Every animation here is decorative. Each one is switched off under
   prefers-reduced-motion at the end of this block, and none of them gate
   content: with motion disabled the page reads exactly the same.
   ========================================================================== */

.aihero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: clamp(var(--s-6), 5vw, var(--s-10));
  align-items: center;
}

.aihero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-8);
}

/* -- The receipt ---------------------------------------------------------- */

/* A price list drawn as a paper receipt. The whole argument of the page is
   "you can see the cost before you spend it", and a receipt is the one object
   that means exactly that without needing a caption. */
.receipt {
  position: relative;
  padding: var(--s-6) var(--s-6) var(--s-8);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-3);
  rotate: -1.2deg;
}

.receipt__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  padding-bottom: var(--s-3);
  border-bottom: 2px dashed var(--ink-20);
}

.receipt__title {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.receipt__unit {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-60);
}

.receipt__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.receipt__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  padding-block: 11px;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}

/* The dotted leader between label and price. Pure decoration, and the reason
   the two columns read as one line rather than two. */
.receipt__row::before {
  content: "";
  order: 1;
  flex: 1 1 auto;
  align-self: center;
  height: 0;
  border-bottom: 2px dotted var(--ink-20);
}

.receipt__row > span {
  flex: 0 1 auto;
}

.receipt__row > b {
  order: 2;
  flex: none;
  font-variant-numeric: tabular-nums;
  font-weight: 800;
}

.receipt__row--free > b {
  color: var(--accent-strong);
  text-transform: uppercase;
  font-size: 13px;
  letter-spacing: 0.5px;
}

.receipt__foot {
  margin-top: var(--s-4);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-60);
  text-align: center;
}

/* The torn bottom edge, drawn with two zig-zag gradients rather than an
   image, so it inherits the ink colour and costs no request. */
.receipt::after {
  content: "";
  position: absolute;
  inset-inline: -1px;
  bottom: -11px;
  height: 12px;
  background:
    linear-gradient(-45deg, transparent 8px, var(--ink) 8px, var(--ink) 10px, var(--surface-2) 10px) 0 0 / 20px 12px repeat-x,
    linear-gradient(45deg, transparent 8px, var(--ink) 8px, var(--ink) 10px, var(--surface-2) 10px) 10px 0 / 20px 12px repeat-x;
}

/* -- The split ------------------------------------------------------------ */

.aisplit {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 56px minmax(0, 1fr);
  gap: var(--s-4);
  align-items: stretch;
  margin-top: var(--s-10);
}

.aisplit__side {
  padding: var(--s-7);
  border: var(--stroke);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
}

.aisplit__side--local {
  background: var(--tile-mint);
}

.aisplit__side--cloud {
  background: var(--tile-purple);
}

.aisplit__tag {
  display: inline-block;
  margin-bottom: var(--s-3);
  padding: 4px 12px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

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

.aisplit__facts li {
  position: relative;
  padding-left: 26px;
  font-size: 14px;
  font-weight: 600;
}

.aisplit__facts li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 4px;
  width: 14px;
  height: 14px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--surface-2);
}

.aisplit__seam {
  position: relative;
  display: grid;
  place-items: center;
}

.aisplit__seam svg {
  width: 100%;
  height: 100%;
}

.aisplit__seam-badge {
  position: absolute;
  padding: 5px 10px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--pop-sun);
  box-shadow: var(--shadow-1);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* -- Scenarios ------------------------------------------------------------ */

.usecases {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  margin-top: var(--s-10);
}

.usecase {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--base) var(--ease), translate var(--base) var(--ease);
}

.usecase:hover {
  box-shadow: var(--shadow-2);
  translate: -3px -3px;
}

.usecase__art {
  border: var(--stroke);
  border-radius: var(--r-md);
  background: var(--ground);
  overflow: clip;
}

.usecase__art svg {
  display: block;
  width: 100%;
  height: auto;
}

.usecase__price {
  align-self: flex-start;
  padding: 4px 12px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--tile-purple);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.usecase__price--free {
  background: var(--pop-mint);
}

/* -- Price table on this page --------------------------------------------- */

.cmp--ai {
  min-width: 0;
}

.cmp--ai th[scope="row"] {
  text-align: left;
}

.cmp--ai tbody tr {
  transition: background var(--fast) var(--ease);
}

.cmp--ai tbody tr:hover {
  background: var(--surface);
}

.cmp--ai .cmp-row--free th,
.cmp--ai .cmp-row--free td {
  background: var(--tile-mint);
  font-weight: 700;
}

/* -- Motion ---------------------------------------------------------------

   The scenes idle until their card is on screen, then play once. app.js sets
   .reveal-ready only when the visitor has not asked for reduced motion, so
   gating on it means one switch governs the lot; the block at the end of this
   file is the belt to that pair of braces. */

.reveal-ready .usecase .usecase__flow {
  stroke-dasharray: 120;
  stroke-dashoffset: 120;
}

.reveal-ready .usecase.is-visible .usecase__flow {
  animation: uc-draw 620ms var(--ease-out) 220ms both;
}

.reveal-ready .usecase .usecase__out {
  opacity: 0;
}

.reveal-ready .usecase.is-visible .usecase__out {
  animation: uc-land 460ms var(--ease-out) 700ms both;
}

.reveal-ready .usecase.is-visible .usecase__stack {
  animation: uc-fan 700ms var(--ease-out) 160ms both;
}

.reveal-ready .usecase.is-visible .usecase__beam {
  animation: uc-scan 1900ms var(--ease) 500ms 2 alternate both;
}

.reveal-ready .aisplit__seam-line {
  stroke-dasharray: 320;
}

.reveal-ready .aisplit__side.is-visible ~ .aisplit__seam .aisplit__seam-line {
  animation: seam-draw 900ms var(--ease-out) both;
}

@keyframes uc-draw {
  from { stroke-dashoffset: 120; }
  to { stroke-dashoffset: 0; }
}

@keyframes uc-land {
  0% { opacity: 0; translate: 14px 0; }
  100% { opacity: 1; translate: 0 0; }
}

@keyframes uc-fan {
  0% { opacity: 0; scale: 0.94; }
  100% { opacity: 1; scale: 1; }
}

@keyframes uc-scan {
  from { transform: translateY(0); }
  to { transform: translateY(84px); }
}

@keyframes seam-draw {
  from { stroke-dashoffset: 320; }
  to { stroke-dashoffset: 0; }
}

/* -- Small screens -------------------------------------------------------- */

@media (max-width: 900px) {
  .aihero__grid {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The receipt stops being a sidebar and becomes a block, so the tilt that
     read as charm beside the headline would just look broken above it. */
  .receipt {
    rotate: 0deg;
  }

  .aisplit {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The seam turns from a column into a divider between two stacked halves. */
  .aisplit__seam {
    height: 56px;
  }

  .aisplit__seam svg {
    height: 100%;
    rotate: 90deg;
  }
}

@media (prefers-reduced-motion: reduce) {
  .usecase:hover {
    translate: 0 0;
  }

  .reveal-ready .usecase .usecase__flow,
  .reveal-ready .usecase.is-visible .usecase__flow {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
    animation: none;
  }

  .reveal-ready .usecase .usecase__out,
  .reveal-ready .usecase.is-visible .usecase__out {
    opacity: 1;
    animation: none;
  }

  .reveal-ready .usecase.is-visible .usecase__stack,
  .reveal-ready .aisplit__side.is-visible ~ .aisplit__seam .aisplit__seam-line {
    animation: none;
  }

  /* The scanning beam has nowhere to go without motion, so it goes away
     rather than sitting frozen over the text it was meant to sweep. */
  .reveal-ready .usecase.is-visible .usecase__beam {
    opacity: 0;
    animation: none;
  }
}

/* ==========================================================================
   Violet header

   The bar and its wave are one surface, so both take the accent. Everything
   that sits on it has to be re-stated: ink on --accent measures 3.67:1, under
   the floor for any text, while white lands at 4.93:1. So the wordmark, the
   links and the controls all go white, and the states that used to be carried
   by "darken towards ink" need somewhere else to go.

   Opaque, not 94% over a blur. The footer is the same bar at the other end of
   the page and it cannot be translucent — nothing sits behind it — so a
   translucent header made the two ends of every page measurably different
   violets. One value, both ends.
   ========================================================================== */

.header {
  background: var(--accent);
}

/* The wave is an extension of the bar's own surface — it has to repeat the
   background exactly or a seam appears where the two meet. */
.header::before {
  background: var(--accent);
}

/* The crest line stays ink. Against violet above and white below it is the
   one edge that reads on both sides. */

.header .brand {
  color: #ffffff;
}

/* The wordmark's drawn mark is stroked with --ink, which vanishes on violet. */
.header .brand__mark path[stroke] {
  stroke: #ffffff;
}

.header .nav__link {
  color: #ffffff;
}

/* Hover and current page cannot be "more white". They get a drawn underline
   instead, which is also the more honest signal: a state carried by colour
   alone is invisible to anyone who cannot separate these two violets. */
.header .nav__link::after {
  content: "";
  position: absolute;
  left: var(--s-3);
  right: var(--s-3);
  bottom: 2px;
  height: 2px;
  border-radius: 2px;
  background: #ffffff;
  scale: 0 1;
  transform-origin: left center;
  transition: scale var(--fast) var(--ease);
}

.header .nav__link:hover,
.header .nav__link[aria-current="page"] {
  color: #ffffff;
}

.header .nav__link:hover::after,
.header .nav__link[aria-current="page"]::after {
  scale: 1 1;
}

/* The language control: outlined in white rather than filled, so it reads as
   secondary next to the account button without needing a second colour. */
.header .lang__button {
  border-color: #ffffff;
  background: transparent;
  color: #ffffff;
  box-shadow: none;
}

.header .lang__button:hover {
  background: rgba(255, 255, 255, 0.16);
}

/* The account button was --accent on --accent: violet on violet, invisible.
   It goes to the sun yellow, which is the one colour on the site's palette
   that is neither the bar nor the page behind it — so it stays the loudest
   thing in the bar from either side of the wave. Ink on it measures 12.6:1,
   far above the floor, and the ink outline holds the shape against white
   once the bar's wave has been scrolled past. */
.header .btn--primary {
  --btn-bg: var(--pop-sun);
  --btn-fg: var(--ink);
  --btn-line: var(--ink);
}

.header .btn--primary:hover {
  --btn-bg: #ffffff;
}

/* The burger's three bars are ink by default. */
.header .burger__box span {
  background: #ffffff;
}

/* Focus rings are drawn from --accent elsewhere; on the accent itself they
   would be invisible, so inside the bar they are white with an ink halo that
   works whichever side of the wave the control sits on. */
.header a:focus-visible,
.header button:focus-visible {
  outline: 3px solid #ffffff;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .header .nav__link::after {
    transition: none;
  }
}

/* ==========================================================================
   Header sizing

   Matched to Creem's bar: 60px tall, 14px links, regular weight, no caps.
   Ceyu's ran 68px with 15px uppercase bold links, which reads a good deal
   heavier than the reference at the same nominal size — uppercase and 700
   both add optical weight that a pixel value does not show.

   Vertical only. Nothing here touches the horizontal rhythm.
   ========================================================================== */

:root {
  --header-h: 60px;
}

/* Two steps up from the 14px this used to be. At that size the navigation was
   the smallest text in the bar — smaller than the account button beside it —
   which put the primary way around the site below the primary call to action
   in the reading order.

   The existing fold-to-burger breakpoints still hold at this size: measured
   across both languages, 360px to 1920px, the bar's contents never reach its
   right edge. German is the tight case, and it clears too. */
.header .nav__link {
  padding-block: var(--s-1);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.2px;
  text-transform: none;
}

/* The wave scales with the bar. 16px under a 68px bar was already the taller
   end of what the crest can carry; under a 60px bar it would dominate the
   furniture it hangs from. */
.header::before,
.header::after {
  height: 13px;
}

@media (max-width: 620px) {
  .header::before,
  .header::after {
    height: 7px;
  }
}

/* The controls shrink with it, or a 44px button inside a 60px bar leaves no
   air above and below. These stay at the 24px target-size floor. */
.header .btn {
  min-height: 36px;
  padding-block: 8px;
  font-size: 13px;
}

.header .lang__button {
  padding-block: 6px;
  font-size: 13px;
}

.header .brand {
  font-size: 27px;
}

@media (max-width: 900px) {
  .header .brand {
    font-size: 23px;
  }
}



/* The form arrives rather than appears. The delay is per position in the
   sheet, which is why it is written as nth-child and not handed over from
   script: the order on screen is the order in the markup, and one file
   should not have to know what the other decided. `both` holds the first
   frame, so nothing flashes at full opacity before its turn. */
.modal[open] .support-form > *,
.modal[open] .modal__lead {
  animation: sheet-in 460ms var(--ease-out) both;
}

.modal[open] .modal__lead {
  animation-delay: 40ms;
}

.modal[open] .support-form > *:nth-child(1) { animation-delay: 90ms; }
.modal[open] .support-form > *:nth-child(2) { animation-delay: 130ms; }
.modal[open] .support-form > *:nth-child(3) { animation-delay: 170ms; }
.modal[open] .support-form > *:nth-child(4) { animation-delay: 210ms; }
.modal[open] .support-form > *:nth-child(5) { animation-delay: 250ms; }
.modal[open] .support-form > *:nth-child(6) { animation-delay: 290ms; }
.modal[open] .support-form > *:nth-child(n + 7) { animation-delay: 330ms; }

@keyframes sheet-in {
  from {
    opacity: 0;
    translate: 0 12px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .modal[open] .support-form > *,
  .modal[open] .modal__lead {
    animation: none;
  }
}

/* The saving, in money. It sits directly under the team total because that is
   the pair a reader compares: what this costs, and what the seat count just
   took off it. Sun yellow with an ink outline is the same sticker the seat
   picker uses for the same fact, so the two read as one mechanism rather than
   two unrelated discounts. */
.plan__save {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--s-2);
  padding: 4px 10px;
  border: 1.5px solid var(--ink);
  border-radius: var(--r-round);
  background: var(--pop-sun);
  color: var(--ink);
  font-size: 12px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.plan__save[hidden] {
  display: none;
}

/* Small print under the call to action. The trial disclosure used to sit in
   the middle of the price block, where it was the longest line on the card and
   the first thing the eye tripped over. It is the same sentence, shortened,
   in the place a reader looks for terms — under the button they apply to. */
.plan__fine {
  margin-bottom: var(--s-3);
  color: var(--ink-60);
  font-size: 11.5px;
  line-height: 1.45;
  text-align: center;
}

.plan__fine[hidden] {
  display: none;
}

/* The unavailable call to action on the recommended card. At 55% opacity over
   the violet it came out as pale lilac on violet — the one element on the page
   that has to be read at a glance, and it was the least legible thing on the
   card. Given its own colours instead of inheriting a rule written for white
   cards: an ink plate the outline can sit on, and white text at full strength.
   It still reads as unavailable, because it is flat where a live button is
   raised, and it still takes no pointer. */
.plans > .plan--featured .btn.is-disabled {
  opacity: 1;
  background: rgba(21, 22, 23, 0.42);
  border-color: rgba(255, 255, 255, 0.55);
  color: #ffffff;
}

/* The list price and its rate are one object; they never split across lines. */
.plan__anchor {
  display: inline-block;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Form validation — said in the page's own voice

   The browser's own bubble is a grey tooltip in the operating system's font
   that vanishes on the next click. These marks stay until the field is fixed,
   and they are drawn the way the rest of the site is drawn.
   -------------------------------------------------------------------------- */

.field[data-invalid] :is(input, textarea),
.field[data-invalid] .select-x__combo {
  border-color: var(--danger-ink, #9f241f);
  box-shadow: 0 0 0 4px rgb(159 36 31 / 0.16), 3px 3px 0 var(--danger-ink, #9f241f);
}

.field[data-invalid] label {
  color: var(--danger-ink, #9f241f);
}

.field__error {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  color: var(--danger-ink, #9f241f);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1.4;
}

.field__error svg {
  flex: none;
  margin-top: 1px;
}

/* One shake, then it stops. A mark that keeps moving is harder to read than
   the message it is drawing attention to. */
.field.is-shaking :is(input, textarea, .select-x) {
  animation: field-shake 420ms var(--ease);
}

@keyframes field-shake {
  0%,
  100% {
    translate: 0 0;
  }
  15% {
    translate: -7px 0;
  }
  32% {
    translate: 6px 0;
  }
  49% {
    translate: -4px 0;
  }
  66% {
    translate: 3px 0;
  }
  83% {
    translate: -1px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .field.is-shaking :is(input, textarea, .select-x) {
    animation: none;
  }
}

/* The action row keeps its own breathing room. The small figure that used to
   perch on this edge is gone: the page is white, and the button speaks for
   itself. */
.form-actions {
  position: relative;
}


/* A calculator that has been asked something with no answer says so, in place
   of the number it would otherwise invent. */
.calc__warn {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: var(--s-3);
  padding: 12px 14px;
  border: 2px solid var(--danger-ink, #9f241f);
  border-radius: var(--r-md);
  background: #fff5f4;
  color: var(--danger-ink, #9f241f);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1.45;
}

.calc__warn svg {
  flex: none;
  margin-top: 1px;
}

.calc__warn[hidden] {
  display: none;
}

/* While the answer is undefined the inputs stay live — the reader is one
   number away from a real result, and greying the controls would hide that. */
.calc[data-blocked] .calc__result-value {
  color: var(--ink-40);
}

/* --------------------------------------------------------------------------
   Calculator hub — the journal grid, with the arithmetic's own drawing

   The cards are post-cards: same frame, same hover, same search and category
   chips driven by the same code. What differs is the picture, which is the
   shape of the chart that calculator draws — bars compare, a ring splits a
   whole, a line runs over time — so the grid says something about the tools
   before anyone reads a title.
   -------------------------------------------------------------------------- */

.calc-card {
  padding: 0;
}

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

.calc-card .post-card__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--s-3);
}

.calc-card .post-card__title {
  display: block;
  font-size: var(--t-h4);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

.calc-card .post-card__excerpt {
  display: block;
  color: var(--ink-70);
}

.calc-card__go {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
  padding-top: var(--s-2);
  color: var(--accent-strong);
  font-size: var(--t-sm);
  font-weight: 800;
}

.calc-card__go svg {
  width: 18px;
  height: 18px;
  transition: translate var(--fast) var(--ease);
}

.calc-card:hover .calc-card__go svg {
  translate: 4px 0;
}

/* One tint per group, so the four categories are legible as colour before
   anybody reads the label on the card. */
.calc-cover--money { background: var(--tile-sun); }
.calc-cover--tools { background: var(--tile-purple); }
.calc-cover--delivery { background: var(--tile-mint); }
.calc-cover--runway { background: var(--tile-sky); }

.calc-art__bars {
  position: absolute;
  z-index: 2;
  right: 14%;
  bottom: 20%;
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 42%;
}

.calc-art__bars i {
  width: 15px;
  border: var(--stroke);
  border-radius: 4px 4px 0 0;
  background: var(--surface-2);
}

.calc-art__bars i:nth-child(1) { height: 42%; background: var(--pop-sky); }
.calc-art__bars i:nth-child(2) { height: 72%; background: var(--pop-sun); }
.calc-art__bars i:nth-child(3) { height: 100%; background: var(--accent); }

.calc-art__donut {
  position: absolute;
  z-index: 2;
  right: 15%;
  bottom: 18%;
  width: 27%;
  aspect-ratio: 1;
  border: var(--stroke);
  border-radius: 50%;
  /* Drawn rather than charted: two thirds accent, one third sun, with the
     hole punched out by a mask so the ink outline survives. */
  background: conic-gradient(
    var(--accent) 0 62%,
    var(--pop-sun) 62% 100%
  );
  mask: radial-gradient(circle, transparent 0 38%, #000 38.5%);
}

.calc-art__line {
  position: absolute;
  z-index: 2;
  right: 12%;
  bottom: 20%;
  width: 44%;
  height: 30%;
}

.calc-art__line svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The four groups under the grid: a plain, crawlable list of everything, with
   the sentence that says why the group exists. */
.calc-groups {
  display: grid;
  gap: var(--s-8);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  margin-top: var(--s-16);
  padding-top: var(--s-10);
  border-top: var(--stroke);
}

.calc-group p {
  margin-top: var(--s-2);
  color: var(--ink-70);
  font-size: var(--t-sm);
}

.calc-group ul {
  margin-top: var(--s-4);
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--s-2);
}

.calc-group a {
  font-weight: 600;
}

/* ==========================================================================
   The drawn cast — inline figures, the loading sheet, the one on the dialog

   Small ink characters that live on the page rather than in it. Three rules
   the whole cast obeys:

   1. Nothing here is ever a hit target: every figure is pointer-events:none
      and aria-hidden, so it can sit over a link without eating the click.
   2. The page background stays white. These appear where they mean something
      — beside a question, on the head of a form — and nowhere else.
   3. Every animation stops under prefers-reduced-motion, at the bottom of
      this block. With motion off they are simply still drawings.
   ========================================================================== */

.doodle-inline {
  display: inline-block;
  flex: none;
  width: var(--doodle-size, 52px);
  vertical-align: middle;
  pointer-events: none;
}

/* A heading that stands beside a figure. It wraps rather than squeezing: on
   a narrow screen the drawing drops to its own line instead of shaving
   characters off the words. */
.heading-with-doodle {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-4);
}

/* Empty until app.js draws into it, and it must take no room until then. */
.doodle-slot:empty {
  display: none;
}

/* ── Eyes ──────────────────────────────────────────────────────────────────
   The white and the ring are drawn; the pupil is moved by app.js and the lid
   is a scale on the whole eye, which is cheaper than animating a path and
   reads exactly the same at this size. */

.doodle__pupil {
  transition: translate 90ms linear;
}

/* Following a pointer wants to be immediate; looking around the room does
   not. Same movement, a slower and softer curve. */
.is-wandering .doodle__pupil {
  transition: translate 620ms var(--ease-out);
}

.doodle__eye {
  transform-box: fill-box;
  transform-origin: 50% 50%;
}

.is-blinking .doodle__eye {
  animation: doodle-blink 170ms steps(1, end) 2;
}

@keyframes doodle-blink {
  0%,
  100% {
    scale: 1 1;
  }
  50% {
    scale: 1 0.12;
  }
}

/* ── The one on the dialog ─────────────────────────────────────────────────
   Sits on the top edge of the report sheet, legs over the side, watching the
   pointer. It is here because this is a form somebody reaches when something
   has gone wrong. */

/* Perched on the lower edge of the violet band, legs over the side. Not on
   the top edge: the dialog clips its own corners so a figure above the head
   would be sliced in half — and this way the head reads as something it is
   sitting on rather than as a hat. */
.modal__mascot {
  position: absolute;
  bottom: 0;
  left: var(--s-6);
  width: 58px;
  /* Its feet stand on the rule under the head. The drawing's box ends where
     the toes do, so no translate is needed to seat it — the two per cent is
     the overlap that makes it read as standing on the line rather than
     hovering a hair above it. */
  translate: 0 2%;
  pointer-events: none;
  z-index: 4;
}

/* The exclamation mark keeps its drop shadow inside the box, so its own foot
   sits six units above the bottom edge rather than on it. It is also the
   narrower drawing of the two — half its box is air either side — so it needs
   more width to carry the same weight beside the title. */
.modal__mascot[data-doodle="bang"] {
  /* 53, not 76. The mark's box used to be 100 units wide for a drawing 66
     units across — a third of it was air, so it needed an oversized slot to
     read at the same weight as the figure beside it. The box now ends where
     the ink does, and the same drawing needs less room. */
  width: 53px;
  /* Eight of the box's 114 units sit below the dot: the drop shadow plus the
     two-unit margin. Moving the figure down by that much puts the dot itself
     on the rule and leaves the shadow falling past it onto the sheet, which
     is what a shadow does. */
  translate: 0 7%;
}

/* Room for it, so the title never runs underneath. */
.modal__head:has(.modal__mascot) {
  padding-bottom: var(--s-7);
}

.modal__head:has(.modal__mascot) .modal__title,
.modal__head:has(.modal__mascot) > .h3 {
  padding-left: 76px;
}

/* The figure hangs over the join, so the first line of the sheet starts below
   it rather than behind its feet. */
.modal__head:has(.modal__mascot) + .modal__lead,
.modal__head:has(.modal__mascot) + .support-form {
  margin-top: var(--s-5);
}

@media (prefers-reduced-motion: reduce) {
  .doodle__pupil {
    transition: none;
  }

  .is-blinking .doodle__eye {
    animation: none;
  }
}

/* ==========================================================================
   Calculators — the hub bands and the drawn numbers behind the headings
   ========================================================================== */

/* The tools sit inside the hero now, under the lede, so the first thing on
   screen is the way to find a calculator rather than a paragraph about them. */
/* The tools bar sits at the bottom of the hero, so it needs air above it —
   otherwise it reads as part of the paragraph it follows — and air below, or
   the first row of cards starts before the eye has finished with it. The
   drawn digits in the hero background also crowd it at the old spacing. */
.calc-tools {
  margin-top: var(--s-12);
  margin-bottom: var(--s-8);
  width: 100%;
}

.calc-hero,
.calc-header {
  position: relative;
  isolation: isolate;
}

/* Thirteen calculators in one flat grid is a wall. Grouped, each band says
   what its four are for before you read a single card title. */
.calc-band + .calc-band {
  margin-top: var(--s-12);
}

.calc-band[hidden] {
  display: none;
}

.calc-band__head {
  margin-bottom: var(--s-6);
  padding-bottom: var(--s-4);
  border-bottom: var(--stroke);
}

.calc-band__head p {
  margin-top: var(--s-2);
  max-width: 68ch;
}

/* ── The numbers ───────────────────────────────────────────────────────────
   Loose figures drifting behind the headline, in the ink outline the rest of
   the site is drawn in. Decoration, and marked as such: aria-hidden, never a
   hit target, and never in front of a word — the layer sits at z-index -1
   inside its own stacking context, which is what `isolate` above is for.

   They arrive once, from where they are, and then stand still. */
.calc-digits {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.04em;
  color: transparent;
  -webkit-text-stroke: 2px color-mix(in srgb, var(--accent) 34%, #ffffff);
}

.calc-digits span {
  position: absolute;
  left: var(--x);
  top: var(--y);
  font-size: calc(var(--s, 1) * 42px);
  rotate: var(--r, 0deg);
  translate: -50% -50%;
  animation: digit-in 900ms var(--ease-out) both;
  animation-delay: calc(var(--s, 1) * 120ms);
}

@keyframes digit-in {
  from {
    opacity: 0;
    scale: 0.82;
  }
  to {
    opacity: 1;
    scale: 1;
  }
}

/* On a phone the figures land on top of the words rather than beside them. */
@media (max-width: 900px) {
  .calc-digits {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .calc-digits span {
    animation: none;
  }
}

/* ==========================================================================
   Scrollbars — the site's own, everywhere it is allowed to draw one

   The window, the dialogs, the table scrollers and the plan row all had
   different bars: two of them were styled by hand, the rest were whatever the
   operating system draws, which on Windows is a wide grey slab with square
   arrows at both ends. One rule for all of them.

   Firefox gets `scrollbar-width`/`scrollbar-color`, which is all it exposes.
   Chromium and Safari get the pseudo-elements, which allow the ink outline the
   rest of the site is drawn with. Both roads end at the same bar.
   ========================================================================== */

* {
  scrollbar-width: thin;
  scrollbar-color: var(--ink-20) transparent;
}

*::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

*::-webkit-scrollbar-track {
  background: transparent;
}

/* The thumb is drawn inside its own padding via `background-clip: content-box`
   — a transparent border is the only way to inset a scrollbar thumb, since it
   takes neither margin nor transform. */
*::-webkit-scrollbar-thumb {
  border: 3px solid transparent;
  border-radius: var(--r-round);
  background: var(--ink-20);
  background-clip: content-box;
}

*::-webkit-scrollbar-thumb:hover {
  background: var(--ink-40);
  background-clip: content-box;
}

*::-webkit-scrollbar-thumb:active {
  background: var(--accent);
  background-clip: content-box;
}

/* Where the two axes meet. Left alone it is a small grey square with a raised
   edge, which is the one part of the operating system's bar that survives
   styling the rest. */
*::-webkit-scrollbar-corner {
  background: transparent;
}

/* On the violet band the ink thumb disappears; it is drawn out of the band's
   own light instead. */
.footer *::-webkit-scrollbar-thumb,
.modal__head *::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.45);
  background-clip: content-box;
}

/* The list price, struck, directly under the price it is being discounted
   from. It is the whole of the second line — no rate badge, no saving in
   money, no sentence about billing. Everything a plan card used to say about
   its own price was true and none of it was read. */
.plan__list {
  margin-top: 2px;
  font-size: clamp(15px, 1.4vw, 17px);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.plan__list[hidden] {
  display: none;
}

/* Nothing behind a dialog moves.

   A modal already blocks clicks on the page underneath, but not the wheel:
   scrolling inside the form and reaching its end handed the scroll to the
   document, and the page slid about behind the sheet. `overscroll-behavior`
   on the panel stops the hand-off; this stops the page scrolling at all while
   a dialog is open, including from the keyboard and from a wheel over the
   backdrop. */
html:has(dialog[open]) {
  overflow: hidden;
  /* The scrollbar disappearing with it would shift the whole page sideways by
     its width. Keeping the gutter reserved is what makes the dialog open
     without the page moving. */
  scrollbar-gutter: stable;
}

/* --------------------------------------------------------------------------
   Newsroom, second layout

   These rules are additive on purpose. The page's original markup — the row
   list and the media card — keeps its own rules above, so whichever version of
   newsroom.html is on disk renders correctly. Nothing here removes anything.
   -------------------------------------------------------------------------- */

/* ── The perch ─────────────────────────────────────────────────────────────
   The bird is positioned against the stamp, not against the column, so it
   keeps meeting the same edge when the label is translated into a longer
   word. */

.newsroom-mark {
  position: relative;
  display: inline-block;
  /* Room above for the figure, which overlaps upward into the hero's own
     padding rather than into the header. */
  margin-top: clamp(52px, 6vw, 72px);
  margin-bottom: var(--s-6);
}

.newsroom-mark .bird {
  position: absolute;
  right: 12px;
  /* The toes overlap the top edge of the pill by five pixels. That overlap is
     the whole difference between standing on something and hovering over it —
     and it only lands because the drawing's box ends where the toes do, with
     no empty strip under them. */
  bottom: calc(100% - 5px);
  width: clamp(60px, 6.5vw, 80px);
  z-index: 1;
}

.newsroom-mark .newsroom-stamp {
  display: inline-block;
  padding: 8px 18px;
  font-size: 13px;
  font-weight: 800;
  transition: box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

/* The perch answers when the thing standing on it moves. */
.newsroom-mark:hover .newsroom-stamp {
  box-shadow: var(--shadow-2);
  translate: -1px -1px;
}

/* ── The row of ways in ──────────────────────────────────────────────────── */

.newsroom-jump {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-8);
  padding: 0;
  list-style: none;
}

.newsroom-jump a {
  display: inline-block;
  padding: 8px 16px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  color: var(--ink);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  box-shadow: var(--shadow-1);
  transition: background var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease), translate var(--fast) var(--ease);
}

.newsroom-jump a:hover {
  background: var(--tile-purple);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.newsroom-jump a:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

/* ── The announcements ───────────────────────────────────────────────────── */

.news-grid {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: stretch;
}

.news-card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
  transition: translate var(--base) var(--ease),
    box-shadow var(--base) var(--ease);
}

.news-card:hover {
  translate: -3px -3px;
  box-shadow: var(--shadow-3);
}

/* The band names the kind of change before the headline does. */
.news-card__band {
  padding: 10px 20px;
  border-bottom: var(--stroke);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink);
}

.news-card__band--infra {
  background: var(--tile-sky);
}

.news-card__band--company {
  background: var(--tile-sun);
}

.news-card__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-6) var(--s-6) var(--s-7);
}

.news-card__date {
  color: var(--ink-60);
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.news-card h3 {
  font-size: var(--t-h4);
  line-height: 1.2;
  letter-spacing: -0.02em;
}

.news-card h3 a {
  color: inherit;
}

/* The whole card is the target; the heading link carries the address, so the
   markup stays a heading with a link in it rather than a div nobody can tab
   to. */
.news-card h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Above the rest of the card, or the "see live status" line — which comes
     later in the markup and therefore paints on top — would swallow clicks in
     the bottom third of the card, exactly where the eye is told to aim. */
  z-index: 1;
}

.news-card p {
  color: var(--ink-70);
  font-weight: 500;
}

.news-card__go {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
  padding-top: var(--s-2);
  color: var(--accent-strong);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}

.news-card__go svg {
  width: 18px;
  height: 18px;
  transition: translate var(--fast) var(--ease);
}

.news-card:hover .news-card__go svg {
  translate: 4px 0;
}

/* ── The three tiles underneath ──────────────────────────────────────────── */

.news-tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-4);
  padding: var(--s-7);
}

.news-tile p {
  color: var(--ink-70);
  font-weight: 500;
}

.news-tile .btn {
  margin-top: auto;
}

@media (prefers-reduced-motion: reduce) {
  .news-card,
  .news-card__go svg,
  .newsroom-mark .newsroom-stamp {
    transition: none;
  }

  .news-card:hover,
  .newsroom-mark:hover .newsroom-stamp {
    translate: 0 0;
  }
}

/* One column, but only for the layout that has no card beside the copy. The
   :has() keeps this from touching the other version of the page. */
.newsroom-hero:has(.newsroom-mark) > .wrap {
  grid-template-columns: minmax(0, 1fr);
}

/* Coarse pointers need a reliable physical hit area even where the desktop
   furniture is deliberately compact. Inline prose links keep their native
   flow exception; discrete header, promo, pricing and social controls do not. */
@media (pointer: coarse), (max-width: 780px) {
  .promo__close {
    width: 44px;
    height: 44px;
  }

  .header .brand,
  .header .btn,
  .header .lang__button,
  .cmp-picker__row button {
    min-height: 44px;
  }

  .footer__social a {
    width: 44px;
    height: 44px;
  }
}

/* --------------------------------------------------------------------------
   30. Comparison page — /compare

   One page carries six head-to-head comparisons, which is the whole reason it
   replaced six thin pages that each argued the same case. Three shapes do the
   work:

     .vs-matrix   everything at once, eight columns, Ceyu marked all the way
                  down. Same idea as the plan table on /pricing, but the
                  featured column is the second rather than the fourth.
     .vs-bars     what a board and a plan cost together. A number in a sentence
                  is read; a bar next to a shorter bar is seen.
     .vs-tabs     one comparison at a time. Built from anchors, so before the
                  script runs — and to a crawler that never runs it — every
                  panel is simply on the page.

   The marks are one system across both tables: [data-mark] on the cell, the
   glyph in a span with its own aria-label, an optional caveat in a <small>.
   -------------------------------------------------------------------------- */

.vs-matrix,
.vs-duel {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

.vs-matrix {
  min-width: 900px;
}

.vs-duel {
  min-width: 640px;
}

.vs-matrix th,
.vs-matrix td,
.vs-duel th,
.vs-duel td {
  padding: 14px 18px;
  text-align: center;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

.vs-matrix thead th,
.vs-duel thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-2);
  border-bottom: 2px solid var(--ink);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink);
}

/* The criterion column is the spine of the table: tinted so it reads as the
   question being answered rather than as an eighth product, and pinned so it
   stays legible while the columns scroll past it on a narrow screen. */
.vs-matrix th:first-child,
.vs-matrix td:first-child,
.vs-duel th:first-child,
.vs-duel td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  text-align: left;
  font-weight: 600;
  min-width: 250px;
  background: color-mix(in srgb, var(--accent) 9%, #ffffff);
  color: var(--ink);
}

.vs-matrix thead th:first-child,
.vs-duel thead th:first-child {
  z-index: 3;
  background: color-mix(in srgb, var(--accent) 15%, #ffffff);
  border-start-start-radius: calc(var(--r-xl) - 2px);
}

.vs-matrix thead th:last-child,
.vs-duel thead th:last-child {
  border-start-end-radius: calc(var(--r-xl) - 2px);
}

.vs-matrix tbody tr:nth-child(even) td,
.vs-duel tbody tr:nth-child(even) td {
  background: var(--surface);
}

.vs-matrix tbody tr:nth-child(even) th:first-child,
.vs-duel tbody tr:nth-child(even) th:first-child {
  background: color-mix(in srgb, var(--accent) 15%, #ffffff);
}

/* The Ceyu column is solid violet from the header down, the same device the
   plan table uses for the recommended plan. One column, one colour: the zebra
   does not run through it, because a striped block reads as a fault.

   Written as `tbody tr td.is-featured` rather than the shorter `td.is-featured`
   on purpose. The zebra selector above carries three element names and two
   classes; the short form carries one element name and loses to it on every
   even row, which painted the recommended column in alternating violet and
   near-white and left a white tick on a pale lavender cell. */
.vs-matrix thead th.is-featured,
.vs-duel thead th.is-featured,
.vs-matrix tbody tr td.is-featured,
.vs-duel tbody tr td.is-featured,
.vs-matrix tfoot tr td.is-featured {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
}

.vs-matrix tbody tr td.is-featured small,
.vs-duel tbody tr td.is-featured small,
.vs-matrix tfoot tr td.is-featured small {
  color: color-mix(in srgb, var(--on-accent) 82%, transparent);
}

.vs-matrix tbody tr:hover td,
.vs-duel tbody tr:hover td {
  background: var(--surface);
}

.vs-matrix tbody tr:hover td.is-featured,
.vs-duel tbody tr:hover td.is-featured {
  background: color-mix(in srgb, var(--accent) 88%, #ffffff);
}

.vs-matrix tbody tr:hover th:first-child,
.vs-duel tbody tr:hover th:first-child {
  background: color-mix(in srgb, var(--accent) 20%, #ffffff);
}

/* The three marks. Sized apart as well as coloured apart, so the answer
   survives a squint from a metre away — the tick is the loudest thing in the
   row, the dash the quietest. */
[data-mark] > span[aria-label] {
  display: inline-block;
  font-weight: 800;
  line-height: 1;
}

[data-mark="yes"] > span {
  font-size: 20px;
  color: var(--accent-strong);
}

.is-featured[data-mark="yes"] > span {
  color: var(--on-accent);
}

[data-mark="part"] > span {
  font-size: 17px;
  color: #b98600;
}

[data-mark="no"] > span {
  font-size: 17px;
  font-weight: 400;
  color: var(--ink-40);
}

[data-mark] small,
.vs-matrix td small,
.vs-duel td small {
  display: block;
  margin-top: 3px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: 0.01em;
  color: var(--ink-50);
}

/* The badge over the Ceyu column. */
.vs-flag {
  display: block;
  margin: -2px auto 4px;
  padding: 2px 10px;
  width: max-content;
  border-radius: var(--r-round);
  background: var(--ink);
  color: #ffffff;
  font-size: 10px;
  letter-spacing: 0.08em;
}

.vs-matrix tfoot th,
.vs-matrix tfoot td {
  border-bottom: 0;
  border-top: 2px solid var(--ink);
}

.vs-matrix__score td {
  color: var(--ink-50);
  font-weight: 600;
}

.vs-matrix__score td strong {
  font-size: 22px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.vs-matrix__score td.is-featured,
.vs-matrix__score td.is-featured strong {
  color: var(--on-accent);
}

.vs-matrix__price td {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.vs-source {
  margin-top: var(--s-4);
  font-size: var(--t-xs);
  line-height: 1.6;
  color: var(--ink-50);
}

.vs-source a {
  color: var(--ink-70);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.vs-source strong {
  color: var(--ink);
}

/* --- the call to action that sits under every comparison ---------------- */

.vs-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-3) var(--s-5);
  margin-top: var(--s-8);
  text-align: center;
}

.vs-cta__note {
  font-size: var(--t-sm);
  color: var(--ink-50);
}

/* --- price bars --------------------------------------------------------- */

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

.vs-bar {
  display: grid;
  grid-template-columns: minmax(180px, 15rem) 1fr auto;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-4);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.vs-bar.is-us {
  background: var(--accent-06);
  box-shadow: var(--shadow-2);
}

.vs-bar__name {
  font-weight: 700;
  font-size: var(--t-sm);
  line-height: 1.25;
}

.vs-bar__name small {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  font-weight: 500;
  color: var(--ink-50);
}

.vs-bar__track {
  position: relative;
  height: 16px;
  border: 2px solid var(--ink);
  border-radius: var(--r-round);
  background: #ffffff;
  overflow: hidden;
}

/* The fill is laid out at its final width and scaled down on the x axis, so
   the growth is a compositor transform rather than a width animation — the
   same reason the reveal uses translate. app.js adds .is-animated and then
   .is-grown when the list scrolls into view; without the script the bars are
   simply already full, which is the honest fallback for a chart. */
.vs-bar__fill {
  display: block;
  height: 100%;
  width: var(--pct);
  border-radius: var(--r-round);
  background: repeating-linear-gradient(
    -45deg,
    var(--ink-20) 0 6px,
    var(--ink-10) 6px 12px
  );
  transform-origin: left center;
}

.vs-bar.is-us .vs-bar__fill {
  background: var(--accent);
}

.vs-bars.is-animated .vs-bar__fill {
  transform: scaleX(0);
  transition: transform 900ms var(--ease-out);
  transition-delay: var(--bar-delay, 0ms);
}

.vs-bars.is-animated.is-grown .vs-bar__fill {
  transform: scaleX(1);
}

.vs-bar__value {
  font-weight: 800;
  font-size: var(--t-lg);
  font-variant-numeric: tabular-nums;
  min-width: 5ch;
  text-align: right;
}

.vs-bar.is-us .vs-bar__value {
  color: var(--accent-strong);
}

@media (max-width: 700px) {
  .vs-bar {
    grid-template-columns: 1fr auto;
  }

  .vs-bar__track {
    grid-column: 1 / -1;
    order: 3;
  }
}

/* --- one at a time ------------------------------------------------------ */

.vs-tabs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-2);
  margin-bottom: var(--s-8);
}

.vs-tab {
  padding: 10px 18px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  font-weight: 700;
  font-size: var(--t-sm);
  color: var(--ink);
  text-decoration: none;
  transition: background var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease), translate var(--fast) var(--ease),
    color var(--fast) var(--ease);
}

.vs-tab:hover {
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.vs-tab:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

.vs-tab[aria-selected="true"] {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-2);
}

.vs-panel {
  padding: var(--s-6) var(--s-6) var(--s-8);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
}

.vs-panel + .vs-panel {
  margin-top: var(--s-8);
}

/* With the script running there is only ever one panel visible, so that
   margin would become a gap under the last one. */
.vs-panels.is-tabbed .vs-panel + .vs-panel {
  margin-top: 0;
}

.vs-panel:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

/* Entering a panel from the tab strip. Deliberately short: this is a swap,
   not an arrival, and a long transition on a tab makes the control feel
   unresponsive rather than smooth. */
@keyframes vs-panel-in {
  from {
    opacity: 0;
    translate: 0 10px;
  }
  to {
    opacity: 1;
    translate: 0 0;
  }
}

.vs-panels.is-tabbed .vs-panel {
  animation: vs-panel-in 260ms var(--ease-out) both;
}

.vs-panel__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}

.vs-panel__head .h3 {
  margin: 0;
}

.vs-verdict {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  margin: 0;
  padding: 6px 14px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--accent-06);
  font-size: var(--t-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-60);
}

.vs-verdict strong {
  color: var(--accent-strong);
}

.vs-lede {
  margin: 0 0 var(--s-6);
  font-size: var(--t-lg);
  line-height: 1.55;
  color: var(--ink-70);
  max-width: 68ch;
}

.vs-lede strong {
  color: var(--ink);
}

.vs-panel .table-scroll {
  margin-bottom: var(--s-5);
}

/* "Where they are ahead" is a real section, not a disclaimer set in grey
   11px. A comparison that concedes nothing is not believed, and the
   concession is also what makes the rest of the page defensible. */
.vs-fair {
  border: 2px dashed var(--ink-20);
  border-radius: var(--r-lg);
  background: var(--surface);
}

.vs-fair > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: 14px 18px;
  cursor: pointer;
  font-weight: 700;
  font-size: var(--t-sm);
  list-style: none;
}

.vs-fair > summary::-webkit-details-marker {
  display: none;
}

.vs-fair > summary .chev {
  flex: none;
  width: 18px;
  height: 18px;
  color: var(--ink-50);
  transition: rotate var(--base) var(--ease);
}

.vs-fair[open] > summary .chev {
  rotate: 45deg;
}

.vs-fair > .answer {
  padding: 0 18px 16px;
  color: var(--ink-70);
  font-size: var(--t-sm);
  line-height: 1.65;
}

.vs-fair > .answer p {
  margin: 0;
  max-width: 68ch;
}

@media (max-width: 700px) {
  .vs-panel {
    padding: var(--s-5) var(--s-4) var(--s-6);
  }

  .vs-lede {
    font-size: var(--t-base);
  }
}

@media (prefers-reduced-motion: reduce) {
  .vs-bars.is-animated .vs-bar__fill,
  .vs-bars.is-animated.is-grown .vs-bar__fill {
    transform: none;
    transition: none;
  }

  .vs-panels.is-tabbed .vs-panel {
    animation: none;
  }

  .vs-fair > summary .chev {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   31. FAQ page — groups, jump list, the figure that asks

   Thirty-eight questions need structure a reader can skim. Three things do
   that: a jump list at the top, a heading per group with room around it, and
   a one-line note under each heading saying what the group covers, so the
   choice is made before the accordion is opened rather than by opening six of
   them.

   The heading used to sit one small step above its accordion, which read as a
   label on the first question rather than as a heading over all of them.
   -------------------------------------------------------------------------- */

.faq-jump {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-16);
}

.faq-jump a {
  padding: 9px 16px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  font-size: var(--t-sm);
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  transition: box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease), background var(--fast) var(--ease),
    color var(--fast) var(--ease);
}

.faq-jump a:hover {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.faq-jump a:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

.faq-group + .faq-group {
  margin-top: clamp(3.5rem, 7vw, 5.5rem);
}

/* The air the heading needed. Scroll-margin so a jump from the list above
   does not park the heading under the sticky site header. */
.faq-group__head {
  margin-bottom: var(--s-8);
  scroll-margin-top: calc(var(--header-h) + var(--s-8));
}

.faq-group {
  scroll-margin-top: calc(var(--header-h) + var(--s-8));
}

.faq-group__head .h3 {
  margin: 0 0 var(--s-2);
}

.faq-group__note {
  margin: 0;
  max-width: 60ch;
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--ink-60);
}

/* The question mark, top right of the hero. Positioned against .hero__inner
   rather than against .hero: the section runs the full width of the window,
   so `right` measured from it would put the figure out by the scrollbar on a
   wide screen instead of on the page's own right edge. Hidden below the
   breakpoint where the hero text would run under it. */
.faq-doodle {
  position: absolute;
  top: 0;
  /* Off the very edge of the text column. At `right: 0` it sat flush with the
     measure and read as part of the paragraph block rather than as something
     standing beside the page. */
  right: clamp(1rem, 5vw, 4.5rem);
  z-index: 1;
  pointer-events: none;
}

@media (max-width: 900px) {
  .faq-doodle {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   32. Changelog — release notes as posts, and the three newest on /roadmap

   A version number over a bullet list is a record. A post — headline, one
   sentence of what it means, the two lines worth skimming, the full note
   behind a click — is something a reader goes through, which is the shape a
   game update note has had for twenty years because it works.

   Each post is a <details>, so opening one costs no script and the whole note
   is in the document for a crawler. The rail above the list is the index once
   there are more releases than fit on a screen.
   -------------------------------------------------------------------------- */

.cl-rail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-10);
}

.cl-rail a {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 14px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  color: var(--ink);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  transition: background var(--fast) var(--ease),
    color var(--fast) var(--ease), box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease);
}

.cl-rail a span {
  font-size: var(--t-sm);
  font-weight: 800;
}

.cl-rail a time {
  font-size: var(--t-xs);
  color: var(--ink-50);
}

.cl-rail a:hover {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.cl-rail a:hover time {
  color: color-mix(in srgb, var(--on-accent) 80%, transparent);
}

.cl-list {
  display: grid;
  gap: var(--s-5);
}

.cl-post {
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  overflow: hidden;
  scroll-margin-top: calc(var(--header-h) + var(--s-6));
}

/* The whole header is the hit target, because on a card this size a small
   chevron in the corner is a target you have to aim at. */
.cl-post__head {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-6);
  cursor: pointer;
  list-style: none;
  transition: background var(--fast) var(--ease);
}

.cl-post__head::-webkit-details-marker {
  display: none;
}

.cl-post__head:hover {
  background: var(--accent-06);
}

.cl-post__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
}

.cl-post__version {
  padding: 3px 12px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--t-xs);
  font-weight: 800;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

.cl-post__date {
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-50);
}

.cl-post__title {
  font-size: var(--t-h3);
  font-weight: 800;
  line-height: 1.15;
  color: var(--ink);
}

.cl-post__lede {
  max-width: 62ch;
  font-size: var(--t-lg);
  line-height: 1.5;
  color: var(--ink-70);
}

/* Two lines of what is inside, so the decision to open is made on content
   rather than on a version number. Hidden once the post is open — they are
   the first two bullets of the note below. */
.cl-post__peek {
  display: grid;
  gap: 6px;
  margin-top: var(--s-2);
}

.cl-post__peek span {
  position: relative;
  padding-left: 20px;
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--ink-60);
}

.cl-post__peek span::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 0.55em;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

.cl-post[open] .cl-post__peek {
  display: none;
}

.cl-post__more {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--s-2);
  font-size: var(--t-sm);
  font-weight: 700;
  color: var(--accent-strong);
}

.cl-post__more svg {
  width: 16px;
  height: 16px;
  transition: rotate var(--base) var(--ease);
}

.cl-post[open] .cl-post__more svg {
  rotate: 45deg;
}

.cl-post__more-close,
.cl-post[open] .cl-post__more-open {
  display: none;
}

.cl-post[open] .cl-post__more-close {
  display: inline;
}

.cl-post__body {
  padding: 0 var(--s-6) var(--s-6);
  display: grid;
  gap: var(--s-5);
  border-top: 2px dashed var(--ink-20);
  margin-top: 0;
  padding-top: var(--s-5);
}

.cl-sec__name {
  margin: 0 0 var(--s-3);
  font-size: var(--t-xs);
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
}

/* The section names carry a colour each, so a note is skimmable by kind:
   what is new, what got better, what was broken. Unknown names fall back to
   the ink chip, which is why this is a tint and not a required list. */
.cl-sec__name::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 8px;
  border: 2px solid var(--ink);
  border-radius: 3px;
  background: var(--ink-20);
  vertical-align: -1px;
}

.cl-sec__name[data-kind="added"]::before {
  background: var(--pop-mint);
}

.cl-sec__name[data-kind="improved"]::before,
.cl-sec__name[data-kind="changed"]::before {
  background: var(--pop-sky);
}

.cl-sec__name[data-kind="fixed"]::before {
  background: var(--pop-sun);
}

.cl-sec__name[data-kind="security"]::before {
  background: var(--pop-coral);
}

.cl-sec__list {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.cl-sec__list li {
  position: relative;
  padding-left: 22px;
  max-width: 70ch;
  font-size: var(--t-base);
  line-height: 1.6;
  color: var(--ink-70);
}

.cl-sec__list li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 0.62em;
  width: 8px;
  height: 8px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--surface-2);
}

.cl-note {
  margin-top: var(--s-8);
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--ink-50);
}

.cl-note a {
  color: var(--ink-70);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* --- the three newest, at the foot of /roadmap ------------------------- */

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

@media (min-width: 860px) {
  .cl-latest {
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  }
}

.cl-latest__item {
  display: grid;
  align-content: start;
  gap: var(--s-2);
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  color: var(--ink);
  text-decoration: none;
  transition: box-shadow var(--fast) var(--ease),
    translate var(--fast) var(--ease), background var(--fast) var(--ease);
}

.cl-latest__item:hover {
  background: var(--accent-06);
  box-shadow: var(--shadow-2);
  translate: -2px -2px;
}

.cl-latest__item:active {
  box-shadow: none;
  translate: var(--press) var(--press);
}

.cl-latest__meta {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.cl-latest__version {
  padding: 2px 10px;
  border: var(--stroke);
  border-radius: var(--r-round);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.cl-latest__meta time {
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--ink-50);
}

.cl-latest__title {
  font-size: var(--t-h4);
  font-weight: 800;
  line-height: 1.2;
}

.cl-latest__lede {
  font-size: var(--t-sm);
  line-height: 1.55;
  color: var(--ink-60);
}

.cl-latest__count {
  margin-top: var(--s-2);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent-strong);
}

.cl-more {
  margin-top: var(--s-8);
  text-align: center;
}

@media (max-width: 700px) {
  .cl-post__head,
  .cl-post__body {
    padding-inline: var(--s-4);
  }
}

@media (prefers-reduced-motion: reduce) {
  .cl-post__more svg,
  .cl-rail a,
  .cl-latest__item {
    transition: none;
  }
}

/* --- how the comparison is scored ---------------------------------------
   The method belongs on the page. A comparison whose rules are not stated is
   an advertisement, and an answer engine has no way to weigh it. Four short
   blocks and the full criteria list behind a click. */

.vs-method {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: 1fr;
  margin-bottom: var(--s-6);
}

@media (min-width: 760px) {
  .vs-method {
    grid-template-columns: 1fr 1fr;
  }
}

.vs-method__item {
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.vs-method__item .h4 {
  margin: 0 0 var(--s-2);
  font-size: var(--t-lg);
  line-height: 1.25;
}

.vs-method__item p {
  margin: 0;
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--ink-60);
}

.vs-criteria__list {
  margin: 0;
  padding-left: 1.3em;
  display: grid;
  gap: 8px;
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--ink-70);
}

@media (min-width: 760px) {
  .vs-criteria__list {
    grid-template-columns: 1fr 1fr;
    column-gap: var(--s-8);
  }
}

/* The "of 14" in the verdict pill, so a score is never read as a ratio
   between the two numbers beside it. */
.vs-verdict__of {
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-40);
}

/* Fourteen criteria make the matrix taller than a screen, and a table whose
   column heads have scrolled away is a grid of ticks with nothing to attach
   them to.

   `.table-scroll` sets `overflow-x: auto`, which makes it a scroll container
   in both axes — so `position: sticky` on the head sticks to a box that never
   scrolls vertically and rides out of view with the table. On a screen wide
   enough for the table there is nothing to scroll horizontally anyway, so the
   container is dropped there and the head sticks to the viewport instead,
   parked under the site bar. Below that width the horizontal scroll is worth
   more than the sticky head, and the tfoot repeat below covers the reader who
   makes it to the bottom. */
@media (min-width: 1100px) {
  .table-scroll:has(.vs-matrix) {
    overflow: visible;
  }

  .vs-matrix thead th {
    top: var(--header-h);
  }
}

/* The column heads once more at the foot, so the score and price rows can be
   read without scrolling back up fourteen rows to find out whose column is
   whose. */
.vs-matrix__foothead th {
  border-top: 2px solid var(--ink);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink);
}

.vs-matrix tfoot .vs-matrix__foothead + tr th,
.vs-matrix tfoot .vs-matrix__foothead + tr td {
  border-top: 0;
}

.vs-matrix__foothead th.is-featured {
  background: var(--accent);
  color: var(--on-accent);
}

/* --- the matrix, second pass -------------------------------------------- */

/* The bottom corners. Above 1100px the scroll container is dropped so the
   head can stick to the viewport (see section 30), and with it went the
   `overflow: clip` that used to cut the table to the panel's rounding — so
   the last row painted a square corner over a round one. The cells that sit
   in the corners round themselves, the same way the head cells already do. */
.vs-matrix tfoot tr:last-child th:first-child {
  border-end-start-radius: calc(var(--r-xl) - 2px);
}

.vs-matrix tfoot tr:last-child td:last-child {
  border-end-end-radius: calc(var(--r-xl) - 2px);
}

/* Twenty-five rows need signposts. The group row is a rule with a word on
   it: it says what the next handful of criteria are about without adding a
   column or a second table. */
/* `[colspan]` is not decoration here: it lifts this above the row-label rule
   below, which otherwise ties on specificity and, being later in the file,
   painted the group bar in the label column's lavender. */
.vs-matrix tbody tr.vs-matrix__group th[colspan] {
  position: sticky;
  left: 0;
  text-align: left;
  padding: 10px 18px;
  background: var(--ink);
  color: #ffffff;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  border-bottom: 0;
}

/* The zebra counts rows, and a group row is a row — which put the stripe out
   of step with the criteria under it. Reset it: the tint follows the group
   heading, not the DOM index. */
.vs-matrix tbody tr:nth-child(even) td,
.vs-matrix tbody tr:nth-child(even) th:first-child {
  background: none;
}

.vs-matrix tbody tr td {
  background: var(--surface-2);
}

.vs-matrix tbody tr th:first-child {
  background: color-mix(in srgb, var(--accent) 9%, #ffffff);
}

.vs-matrix tbody tr.is-odd td {
  background: var(--surface);
}

.vs-matrix tbody tr.is-odd th:first-child {
  background: color-mix(in srgb, var(--accent) 15%, #ffffff);
}

/* And the featured column wins over both, spelled out at a specificity the
   striping cannot match. This is the second time the zebra has eaten the
   violet column; the rule is: whenever a row-level background is added, the
   column-level one has to be restated at the same or higher weight. */
.vs-matrix tbody tr td.is-featured,
.vs-matrix tbody tr.is-odd td.is-featured {
  background: var(--accent);
}

.vs-matrix tbody tr:hover td.is-featured,
.vs-matrix tbody tr.is-odd:hover td.is-featured {
  background: color-mix(in srgb, var(--accent) 88%, #ffffff);
}

/* --- the partial mark ---------------------------------------------------- */

/* It was a bare half-circle glyph in yellow-brown, which at 17px read as a
   speck of dirt on the table. It is a badge now: the half circle drawn as
   two halves of a real shape, in the same orange the rest of the site uses
   for "careful", with the caveat set beneath it as the label it always was. */
[data-mark="part"] > span[aria-label] {
  display: inline-grid;
  place-items: center;
  width: 21px;
  height: 21px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: linear-gradient(
    90deg,
    var(--pop-coral) 0 50%,
    #ffffff 50% 100%
  );
  font-size: 0;
  line-height: 0;
  color: transparent;
}

.is-featured[data-mark="part"] > span[aria-label] {
  border-color: #ffffff;
}

[data-mark="part"] small {
  color: var(--ink-60);
  font-weight: 600;
}

.is-featured[data-mark="part"] small {
  color: color-mix(in srgb, var(--on-accent) 85%, transparent);
}

/* --- the call to action -------------------------------------------------- */

/* One button, centred, and nothing beside it. The grey line to its right was
   a second thing to read at the moment the reader has already decided; it
   also pulled the button off centre, which made the section look misaligned
   rather than deliberate. */
.vs-cta {
  justify-content: center;
}

/* --- headings ------------------------------------------------------------ */

/* A centred section head sits over content that is not centred — the FAQ
   list, the panels — and a heading that starts left of that content reads as
   a mistake. Both now start on the same line, and the figure sits directly
   after the words rather than out at the margin. */
.section-head--center .heading-with-doodle {
  gap: var(--s-3);
}

@media (min-width: 621px) {
  #questions .section-head--center,
  #head-to-head .section-head--center {
    align-items: flex-start;
    text-align: left;
    /* 820px is `.faq`'s own max-width (section 15), so the heading and the
       list it belongs to start on the same line. Left-aligning the heading
       inside the full container instead put it 66px to the left of the list,
       which reads as a mistake rather than as a choice. */
    max-width: 820px;
    margin-inline: auto;
  }

  #questions .section-head--center .heading-with-doodle {
    justify-content: flex-start;
    margin-inline: 0;
  }

  /* „One at a time“ ist kurz genug, um mittig über seinem Absatz zu stehen,
     und steht dort besser als links an der Kante. Ohne `width` schrumpft die
     Zeile in der Spalte auf ihre eigene Breite (`align-items: flex-start`
     oben), und ohne `max-width: none` hält `.section-head .h2` sie auf 24ch
     fest — in beiden Fällen kann `justify-content` nichts mehr verrücken.
     Die Überschrift der Fragen daneben bleibt links: sie läuft über zwei
     Zeilen, füllt die Spalte also ohnehin und schöbe die Figur nur an den
     rechten Rand. */
  #head-to-head .section-head--center .heading-with-doodle {
    justify-content: center;
    max-width: none;
    width: 100%;
    margin-inline: 0;
  }

  #questions .section-head--center .lead,
  #head-to-head .section-head--center .lead {
    margin-inline: 0;
  }
}

/* --------------------------------------------------------------------------
   33. Integrations — the ring, and one card per connector

   The ring is ten tiles standing on a circle in 3D. The circle turns; each
   tile turns the other way, at the same speed, so the marks stay square to
   the reader instead of flattening into edges as they pass the sides. That
   counter-rotation is the whole trick, and it is why this needs no script.

   It is decoration and nothing else: aria-hidden, pointer-events none, every
   name in it repeated as text below, and gone entirely under reduced motion.
   -------------------------------------------------------------------------- */

.ig-stage {
  perspective: 1500px;
  height: 300px;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.ig-ring {
  position: relative;
  width: 88px;
  height: 88px;
  transform-style: preserve-3d;
  animation: ig-spin 34s linear infinite;
}

.ig-ring__slot {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  /* 360° / 10 tiles = 36° apart, pushed out to the radius. The radius is in
     the transform rather than in a width so the tiles keep their own size.

     `--y` alternates every other tile up and down by 34px, set on the element
     because CSS has no modulo. On a flat ring the horizontal spacing
     collapses as a tile passes the far left or right — the marks piled up at
     the edges twice a turn. Two rows separate them exactly where the
     projection stops doing it. */
  transform: rotateY(calc(var(--i) * 36deg)) translateZ(330px)
    translateY(var(--y, 0));
}

.ig-ring__slot .ig-icon {
  animation: ig-counter 34s linear infinite;
  /* Zeigen darf man darauf. Vergrößert wird über `scale`, nicht über
     `transform`: der Gegendreh der Kachel läuft in `transform`, und wer
     dort hineinschreibt, hält ihn an. Als eigene Eigenschaft legt sich
     die Größe darüber, der Ring dreht ungebremst weiter. */
  pointer-events: auto;
  transition:
    scale 260ms cubic-bezier(0.34, 1.28, 0.5, 1),
    box-shadow 260ms var(--ease),
    border-color 260ms var(--ease);
}

.ig-ring__slot .ig-icon:hover {
  scale: 1.42;
  box-shadow: 5px 5px 0 var(--ink);
  border-color: var(--accent);
}

@keyframes ig-spin {
  to {
    transform: rotateY(360deg);
  }
}

/* The tile's own rotation cancels the ring's. Same duration, same timing
   function, opposite direction — anything else and the marks wobble. */
@keyframes ig-counter {
  to {
    transform: rotateY(-360deg);
  }
}

/* Narrow screens do not have the width for a 230px radius, and a ring that
   overflows its section is worse than no ring. */
@media (max-width: 900px) {
  .ig-stage {
    height: 150px;
  }

  .ig-ring__slot {
    transform: rotateY(calc(var(--i) * 36deg)) translateZ(200px)
      translateY(calc(var(--y, 0px) * 0.6));
  }
}

@media (max-width: 560px) {
  .ig-stage-section {
    display: none;
  }
}

.ig-icon {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  color: var(--brand, var(--ink));
  flex: none;
}

.ig-icon svg {
  width: 32px;
  height: 32px;
}

/* --- one card per connector --------------------------------------------- */

.ig-grid {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: 1fr;
}

@media (min-width: 800px) {
  .ig-grid {
    grid-template-columns: 1fr 1fr;
  }
}

.ig-card {
  display: grid;
  align-content: start;
  gap: var(--s-3);
  padding: var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-xl);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  scroll-margin-top: calc(var(--header-h) + var(--s-6));
}

/* Not built yet, and it has to look it: a dashed frame and no drop shadow,
   so a reader skimming the page can tell the two halves apart without
   reading the badge. */
.ig-card.is-soon {
  border-style: dashed;
  border-color: var(--ink-20);
  background: var(--surface);
  box-shadow: none;
}

.ig-card__head {
  display: flex;
  align-items: center;
  gap: var(--s-4);
}

.ig-card__name {
  margin: 0 0 4px;
  font-size: var(--t-h4);
  font-weight: 800;
  line-height: 1.15;
}

.ig-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin: 0;
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--ink-50);
}

.ig-card__meta > span + span::before {
  content: "·";
  margin-right: 10px;
  color: var(--ink-20);
}

.ig-status {
  padding: 2px 9px;
  border: 2px solid var(--ink);
  border-radius: var(--r-round);
  background: var(--pop-mint);
  color: var(--ink);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.is-soon .ig-status {
  background: var(--pop-sun);
}

.ig-card__what {
  margin: 0;
  font-size: var(--t-base);
  line-height: 1.55;
  color: var(--ink);
}

.ig-card__list {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ig-card__list li {
  position: relative;
  padding-left: 20px;
  font-size: var(--t-sm);
  line-height: 1.55;
  color: var(--ink-60);
}

.ig-card__list li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 0.6em;
  width: 7px;
  height: 7px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--surface-2);
}

.is-soon .ig-card__list li::before {
  background: transparent;
  border-color: var(--ink-20);
}

@media (prefers-reduced-motion: reduce) {
  /* No ring at all rather than a still one: a frozen carousel of ten marks
     is a puzzle, and every name in it is written out below anyway. */
  .ig-stage-section {
    display: none;
  }
}

/* --- integrations, second pass: categories and compact cards ------------- */

/* Everything for one connector sits in one card now, and the card is smaller:
   a header row with the mark, the name and the badge, then the sentence and
   the three lines. The tile-above-the-text version needed a third of the page
   for ten connectors and made the text look like a caption. */
.ig-cat + .ig-cat {
  border-top: 0;
}

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

.ig-cat__head .h2 {
  margin: 0 0 var(--s-2);
}

.ig-cat__intro {
  margin: 0;
  max-width: 58ch;
  font-size: var(--t-base);
  line-height: 1.55;
  color: var(--ink-60);
}

.ig-cat__count {
  margin: var(--s-3) 0 0;
  font-size: var(--t-xs);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-strong);
}

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

@media (min-width: 800px) {
  .ig-grid {
    grid-template-columns: 1fr 1fr;
  }
}

.ig-card {
  display: grid;
  align-content: start;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5) var(--s-5);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  scroll-margin-top: calc(var(--header-h) + var(--s-6));
}

/* Not built yet, and it has to look it: a dashed frame and no drop shadow, so
   the two halves are told apart before the badge is read. */
.ig-card.is-soon {
  border-style: dashed;
  border-color: var(--ink-20);
  background: var(--surface);
  box-shadow: none;
}

.ig-card__head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.ig-card__title {
  display: grid;
  gap: 2px;
  min-width: 0;
  margin-right: auto;
}

.ig-card__name {
  font-size: var(--t-lg);
  font-weight: 800;
  line-height: 1.15;
}

.ig-card__meta {
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-50);
}

/* The mark. A framed chip so an official logo and a drawn one sit at the same
   weight rather than one shouting and the other whispering. */
.ig-icon {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 2px solid var(--ink);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--brand, var(--ink));
  flex: none;
}

.ig-icon svg {
  width: 24px;
  height: 24px;
}

.ig-icon.is-brand img {
  width: 26px;
  height: 26px;
  object-fit: contain;
}

.is-soon .ig-icon {
  border-color: var(--ink-20);
  background: var(--surface-2);
}

.ig-status {
  flex: none;
  padding: 2px 9px;
  border: 2px solid var(--ink);
  border-radius: var(--r-round);
  background: var(--pop-mint);
  color: var(--ink);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.is-soon .ig-status {
  background: var(--pop-sun);
}

.ig-card__what {
  margin: 0;
  font-size: var(--t-sm);
  line-height: 1.55;
  color: var(--ink);
}

.ig-card__list {
  display: grid;
  gap: 7px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ig-card__list li {
  position: relative;
  padding-left: 18px;
  font-size: var(--t-xs);
  line-height: 1.55;
  color: var(--ink-60);
}

.ig-card__list li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 0.55em;
  width: 6px;
  height: 6px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--surface-2);
}

.is-soon .ig-card__list li::before {
  background: transparent;
  border-color: var(--ink-20);
}

/* The ring keeps the larger, shadowed chip — it is the decoration, not the
   list. */
.ig-ring .ig-icon {
  width: 60px;
  height: 60px;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
}

.ig-ring .ig-icon svg {
  width: 30px;
  height: 30px;
}

.ig-ring .ig-icon.is-brand img {
  width: 32px;
  height: 32px;
}

/* --- the plan switch on the comparison matrix ---------------------------- */

/* Both plan answers are in the markup; `data-vs-plan` on the table decides
   which one is shown. Doing it in CSS rather than by rewriting cells means a
   crawler sees both, the switch cannot fall out of step with the score, and
   nothing breaks if the script never runs — the table simply stays on Pro. */
.vs-matrix .vs-plan {
  display: none;
}

.vs-matrix[data-vs-plan="entry"] .vs-plan[data-plan="entry"],
.vs-matrix[data-vs-plan="top"] .vs-plan[data-plan="top"] {
  display: revert;
}

.vs-matrix th .vs-plan,
.vs-matrix tfoot .vs-plan {
  display: none;
}

.vs-matrix[data-vs-plan="entry"] th .vs-plan[data-plan="entry"],
.vs-matrix[data-vs-plan="top"] th .vs-plan[data-plan="top"],
.vs-matrix[data-vs-plan="entry"] tfoot .vs-plan[data-plan="entry"],
.vs-matrix[data-vs-plan="top"] tfoot .vs-plan[data-plan="top"] {
  display: block;
}

/* The mark inside a plan cell is the same glyph as everywhere else, so the
   [data-mark] styling has to reach it through the wrapper as well. */
.vs-matrix td.is-plan .vs-plan[data-mark] > span[aria-label] {
  display: inline-block;
  font-weight: 800;
  line-height: 1;
}

.vs-planpick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-3) var(--s-4);
  margin-bottom: var(--s-6);
}

.vs-planpick__label {
  font-size: var(--t-xs);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-50);
}

/* Ein Schalter, keine zwei Knöpfe: eine Fassung, zwei Hälften und ein Daumen,
   der zwischen ihnen läuft. Der Daumen liegt UNTER der Beschriftung und wird
   per `translate` bewegt — das läuft auf dem Compositor, also flüssig, und die
   Schrift wird dabei nicht neu gesetzt. */
.vs-switch {
  position: relative;
  display: inline-flex;
  /* Gar nicht gepolstert. Jede Polsterung, auch eine von drei Pixeln, laesst
     an der Schmalseite des Daumens eine weiße Sichel stehen, weil dort seine
     Rundung neben der Rundung der Fassung liegt statt in ihr. */
  padding: 0;
  border: var(--stroke);
  border-radius: var(--r-round);
  /* Weiße Fassung, damit die Farbe im Daumen steckt und nicht um ihn herum.
     Die nicht gewählte Hälfte trägt Tinte auf Weiß: 5,0:1. */
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
}

.vs-switch__thumb {
  /* Fuellt die Fassung bis an die Kontur, in beiden Richtungen. Bei halber
     Breite und voller Hoehe hat die Kappe genau den Radius der Innenkante,
     also legt sie sich exakt hinein. */
  position: absolute;
  inset: 0;
  width: 50%;
  border-radius: var(--r-round);
  /* Kein eigener Schatten mehr: er lag zwangslaeufig ueber der Kontur der
     Fassung und war die zweite Quelle des weißen Rands. */
  /* Etwas Überschwung, aber nur ein Hauch: der Daumen soll ankommen und nicht
     nachfedern. Die Farbe wechselt langsamer als der Weg, sonst ist der
     Umschlag von Violett auf Gelb schon vorbei, bevor der Daumen ankommt. */
  transition:
    translate 320ms cubic-bezier(0.34, 1.32, 0.5, 1),
    background-color 380ms var(--ease);
  will-change: translate;
}

/* Der Daumen trägt die Farbe des Tarifs, auf dem er steht: das billige Abo
   im Markenviolett, das teuerste im Gelb des Kontoknopfes. Damit sagt die
   Farbe dasselbe wie die Stellung, und man sieht schon im Augenwinkel, in
   welcher man ist.

   Die Beschriftung dreht mit: auf dem Violett Weiß (4,9:1 — Tinte wären
   3,7:1), auf dem Gelb Tinte (11,5:1 — Weiß wäre 1,8:1). Keine der beiden
   Farben trägt beide Beschriftungen. */
.vs-switch[data-vs-switch="entry"] .vs-switch__thumb {
  background: var(--accent);
}

.vs-switch[data-vs-switch="top"] .vs-switch__thumb {
  translate: 100% 0;
  background: var(--pop-sun);
}

.vs-switch[data-vs-switch="entry"] button[aria-checked="true"] {
  color: var(--on-accent);
}

.vs-switch__options {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.vs-switch button {
  display: grid;
  gap: 0;
  padding: 8px 14px;
  border: 0;
  background: none;
  border-radius: var(--r-round);
  color: var(--ink-60);
  cursor: pointer;
  text-align: center;
  transition: color 260ms var(--ease);
}

.vs-switch button[aria-checked="true"] {
  color: var(--ink);
}

.vs-switch button:focus-visible {
  outline: 3px solid var(--accent-strong);
  outline-offset: 3px;
}

.vs-switch__name {
  font-size: var(--t-xs);
  font-weight: 800;
  line-height: 1.25;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .vs-switch__thumb,
  .vs-switch button {
    transition: none;
  }
}

/* The mark now sits inside a `.vs-plan` wrapper, so `.is-featured[data-mark]`
   no longer matches — the two live on different elements. Without these the
   tick fell back to accent-strong purple on the violet column and read as a
   watermark. Same rule as before, one level deeper. */
.vs-matrix td.is-featured .vs-plan[data-mark="yes"] > span[aria-label] {
  font-size: 20px;
  color: var(--on-accent);
}

.vs-matrix td.is-featured .vs-plan[data-mark="no"] > span[aria-label] {
  font-size: 17px;
  font-weight: 400;
  color: color-mix(in srgb, var(--on-accent) 70%, transparent);
}

.vs-matrix td.is-featured .vs-plan[data-mark="part"] > span[aria-label] {
  border-color: #ffffff;
}

.vs-matrix td.is-featured .vs-plan small {
  color: color-mix(in srgb, var(--on-accent) 82%, transparent);
}


/* --------------------------------------------------------------------------
   34. /security — der SSO-Block

   Hier stand der Tresorwuerfel. Er trug sechs Woerter, die im Abschnitt
   darunter ohnehin als Karten stehen, und drehte sich dafuer durch den halben
   Bildschirm. Weg damit; die Seite faengt jetzt direkt mit der Aussage an.
   -------------------------------------------------------------------------- */

/* --- Single sign-on ------------------------------------------------------ */

.sec-sso {
  display: flex;
  align-items: center;
  gap: clamp(20px, 4vw, 40px);
  padding: clamp(20px, 4vw, 34px);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow-2);
}

.sec-sso__mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
}

.sec-sso__body > .eyebrow {
  margin-bottom: 6px;
}

.sec-sso__body > .h3 {
  margin-bottom: 10px;
}

.sec-sso__actions {
  margin-top: 18px;
}

@media (max-width: 640px) {
  .sec-sso {
    flex-direction: column;
    align-items: flex-start;
  }
}


/* --------------------------------------------------------------------------
   35. /support — drei Wege, drei Zeichen in 3D

   Jede Karte traegt eine Platte, die aus sechs gestapelten Lagen wirklich
   Tiefe hat statt sie mit einem Schatten anzudeuten. Darauf steht das
   Zeichen: Discords echte Marke, drei Blaetter, ein Umschlag mit Klappe.

   Bewegung in zwei Schichten, und das ist der ganze Trick. `__spin` schwebt
   von allein per Keyframes, `__tilt` bekommt die Neigung vom Zeiger als zwei
   Custom Properties. Beides in einem Element hiesse, dass die Animation und
   das Skript sich dieselbe transform-Eigenschaft streitig machen und eins
   von beidem verliert.
   -------------------------------------------------------------------------- */

.sup-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: clamp(16px, 2.4vw, 26px);
}

.sup-card {
  --sup-tint: var(--surface-2);
  /* Die Platte traegt ihre eigene Farbe. Solange die Karte weiss war, war das
     dieselbe; seit zwei Karten voll ausgemalt sind, muss die Platte hell
     bleiben, sonst verschwindet das Zeichen im Untergrund. */
  --sup-face: var(--surface-2);
  --sup-edge: var(--ink);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: clamp(22px, 2.6vw, 30px);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--sup-tint);
  box-shadow: var(--shadow-1);
  color: inherit;
  text-decoration: none;
  isolation: isolate;
  transition:
    transform 260ms var(--ease),
    box-shadow 260ms var(--ease);
}

.sup-card:hover,
.sup-card:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-2);
}

/* Zwei volle Farben statt zweier Tuenchen. Die blassen Kacheltoene liessen
   die Dreierreihe wie dreimal Weiss mit einem Hauch Farbe aussehen; jetzt
   steht in der Mitte Violett und rechts Gelb, und die Dokumentation bleibt
   weiss, damit die Reihe einen ruhigen Anfang hat. */
.sup-card[data-sup="discord"] {
  --sup-tint: var(--accent);
  color: var(--on-accent);
}

.sup-card[data-sup="discord"] .card__text {
  color: color-mix(in srgb, var(--on-accent) 86%, transparent);
}

.sup-card[data-sup="mail"] {
  --sup-tint: var(--pop-sun);
}

/* Auf Gelb ist das Grau der Karten zu blass — dort schreibt die Tinte. */
.sup-card[data-sup="mail"] .card__text {
  color: color-mix(in srgb, var(--ink) 82%, transparent);
}

.sup-card .card__title {
  margin-top: 4px;
}

/* --- die Buehne ----------------------------------------------------------- */

.sup3d {
  display: block;
  width: 124px;
  height: 124px;
  margin-bottom: 4px;
  perspective: 700px;
}

.sup3d__spin,
.sup3d__tilt,
.sup3d__tile {
  display: block;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}

/* Schweben. Drei Karten, drei Verzoegerungen, damit sie nicht im Gleichschritt
   auf und ab gehen wie eine Reihe Kolben. */
.sup3d__spin {
  animation: sup-float 6.5s ease-in-out infinite;
}

.sup-card[data-sup="discord"] .sup3d__spin {
  animation-delay: -2.2s;
}

.sup-card[data-sup="mail"] .sup3d__spin {
  animation-delay: -4.4s;
}

@keyframes sup-float {
  0%,
  100% {
    transform: translate3d(0, 0, 0) rotate(0deg);
  }

  50% {
    transform: translate3d(0, -7px, 0) rotate(-1.5deg);
  }
}

/* Die Neigung. Ohne Zeiger stehen die Platten leicht schraeg, damit man die
   Tiefe auch im Standbild sieht — aber nur leicht. Bei 16/-22 Grad stand die
   Platte so quer, dass das Zeichen darauf perspektivisch verzerrt war und die
   Kante mehr Flaeche einnahm als das Bild. Die Ruhelage in app.js
   (`supportTilt`) traegt dieselben Zahlen und muss mitwandern. */
.sup3d__tilt {
  transform: rotateX(var(--sup-rx, 9deg)) rotateY(var(--sup-ry, -12deg));
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}

.sup-card:hover .sup3d__tilt {
  transition-duration: 120ms;
}

.sup3d__tile {
  position: relative;
}

/* Die Platte. Sechs Lagen zu je zwei Einheiten Tiefe, die hinterste am
   dunkelsten: das ist eine wirkliche Kante im Raum und keine gemalte. */
.sup3d__ply {
  position: absolute;
  inset: 12px;
  border: 2.5px solid var(--ink);
  border-radius: 24px;
  background: var(--sup-face);
}

.sup3d__ply:nth-child(1) {
  transform: translateZ(-12px);
  filter: brightness(0.82);
}

.sup3d__ply:nth-child(2) {
  transform: translateZ(-10px);
  filter: brightness(0.86);
}

.sup3d__ply:nth-child(3) {
  transform: translateZ(-8px);
  filter: brightness(0.9);
}

.sup3d__ply:nth-child(4) {
  transform: translateZ(-6px);
  filter: brightness(0.94);
}

.sup3d__ply:nth-child(5) {
  transform: translateZ(-4px);
  filter: brightness(0.97);
}

.sup3d__ply:nth-child(6) {
  transform: translateZ(-2px);
  background: var(--surface-2);
}

/* --- Discord -------------------------------------------------------------- */

.sup3d__mark {
  position: absolute;
  inset: 0;
  width: 44px;
  height: 44px;
  margin: auto;
  transform: translateZ(16px);
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}

.sup-card:hover .sup3d__mark,
.sup-card:focus-visible .sup3d__mark {
  transform: translateZ(34px) scale(1.06);
}

/* --- Dokumentation: drei Blaetter ---------------------------------------- */

.sup3d__sheet {
  position: absolute;
  inset: 28px 34px;
  border: 2.5px solid var(--ink);
  border-radius: 8px;
  background: #ffffff;
  transition: transform 460ms cubic-bezier(0.22, 1, 0.36, 1);
}

.sup3d__sheet[data-sheet="3"] {
  transform: translate3d(-7px, -5px, 6px) rotate(-8deg);
}

.sup3d__sheet[data-sheet="2"] {
  transform: translate3d(3px, -2px, 12px) rotate(4deg);
}

.sup3d__sheet[data-sheet="1"] {
  display: grid;
  align-content: center;
  gap: 5px;
  padding: 0 9px;
  transform: translateZ(18px);
}

/* Die Zeilen auf dem obersten Blatt. Drei Striche, der letzte kurz, damit es
   als Text gelesen wird und nicht als Gitter. */
.sup3d__sheet[data-sheet="1"] i {
  height: 3px;
  border-radius: 2px;
  background: var(--ink-30, color-mix(in srgb, var(--ink) 30%, transparent));
}

.sup3d__sheet[data-sheet="1"] i:last-child {
  width: 58%;
}

.sup-card:hover .sup3d__sheet[data-sheet="3"],
.sup-card:focus-visible .sup3d__sheet[data-sheet="3"] {
  transform: translate3d(-15px, -9px, 6px) rotate(-15deg);
}

.sup-card:hover .sup3d__sheet[data-sheet="2"],
.sup-card:focus-visible .sup3d__sheet[data-sheet="2"] {
  transform: translate3d(9px, -4px, 12px) rotate(9deg);
}

.sup-card:hover .sup3d__sheet[data-sheet="1"],
.sup-card:focus-visible .sup3d__sheet[data-sheet="1"] {
  transform: translateZ(30px);
}

/* --- Post: ein Umschlag, dessen Klappe aufgeht --------------------------- */

.sup3d__env {
  position: absolute;
  inset: 36px 26px;
  transform-style: preserve-3d;
}

.sup3d__front,
.sup3d__flap,
.sup3d__letter {
  position: absolute;
  inset: 0;
  border: 2.5px solid var(--ink);
  border-radius: 6px;
  background: var(--surface-2);
}

.sup3d__letter {
  inset: -12px 6px 8px;
  background: #ffffff;
  transform: translateZ(6px);
  transition: transform 460ms cubic-bezier(0.22, 1, 0.36, 1);
}

.sup3d__front {
  background: var(--tile-coral);
  transform: translateZ(12px);
}

/* Die Klappe ist ein Dreieck und haengt an der Oberkante. Geschlossen liegt
   sie auf dem Umschlag, beim Zeigen dreht sie nach hinten auf. Als SVG
   gezeichnet, weil eine per clip-path geschnittene Ecke ihre Kontur verliert
   und der Umschlag dann nicht mehr zum Rest der Seite passt. */
.sup3d__flap {
  height: 62%;
  inset-block-end: auto;
  border: 0;
  border-radius: 0;
  background: none;
  transform-origin: top center;
  transform: translateZ(13.5px) rotateX(0deg);
  transition: transform 520ms cubic-bezier(0.34, 1.24, 0.5, 1);
}

.sup3d__flap svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.sup-card:hover .sup3d__flap,
.sup-card:focus-visible .sup3d__flap {
  transform: translateZ(13px) rotateX(-158deg);
}

.sup-card:hover .sup3d__letter,
.sup-card:focus-visible .sup3d__letter {
  transform: translate3d(0, -16px, 6px);
}

/* --- kleiner, ruhiger, zugaenglicher ------------------------------------- */

@media (max-width: 560px) {
  .sup3d {
    width: 88px;
    height: 88px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sup3d__spin {
    animation: none;
  }

  .sup3d__tilt,
  .sup3d__mark,
  .sup3d__sheet,
  .sup3d__flap,
  .sup3d__letter,
  .sup-card {
    transition: none;
  }
}

/* Die Bausteine am Fuss der Vergleichsseite.
 *
 * Die Flaeche hat eine feste Hoehe, weil die Physik eine kennen muss: ein
 * Boden, der mit dem Inhalt waechst, ist kein Boden. `touch-action: none`
 * ist die Bedingung dafuer, dass Ziehen auf dem Telefon zieht statt scrollt —
 * ohne die Zeile faengt der Browser den Zeiger ab, bevor das Skript ihn
 * sieht. */
.blocks-head {
  text-align: center;
  margin-bottom: var(--s-6);
}

.blocks-head .lead {
  margin-inline: auto;
}

.blocks {
  position: relative;
  height: clamp(220px, 27vw, 320px);
  border: var(--stroke);
  border-radius: var(--r-lg);
  background: var(--surface-2, var(--bg));
  box-shadow: var(--shadow-1);
  overflow: hidden;
}

.blocks canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: grab;
}

.blocks canvas.is-holding {
  cursor: grabbing;
}
