/* Commute Connects — site stylesheet (v2, dark)
   One file, no framework. Tokens from repos/commuteconnects/docs/design-system.md (v1.1),
   dark direction per brand/website/03-design-v2.md. Brand hue 22°, warm neutrals hue 20°,
   Inter self-hosted from /assets/fonts.

   The page is dark by default: the semantic tokens (--bg, --fg, …) hold the dark set and
   every component reads them. One class, .paper, re-scopes the same tokens to a light set
   for the legal prose, so the prose/table rules work unchanged on both surfaces. */

/* ---------- Fonts ---------- */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/inter-400.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/inter-500.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/inter-600.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/inter-700.woff2") format("woff2");
}

/* ---------- Tokens ---------- */
:root {
  /* Brand scale (design-system.md §1.1, HSL 22° 82%) */
  --brand-50: #fdefe8;
  --brand-100: #fae0d1;
  --brand-200: #f6c1a2;
  --brand-300: #f1a274;
  --brand-400: #ec7c3c;
  --brand-500: #d65c15;
  --brand-600: #b04c11;
  --brand-700: #8b3c0e;
  --brand-800: #6b310f;
  --brand-900: #4e250e;
  --brand-950: #2e1405;

  /* Warm neutrals, hue 20° */
  --n-0: #ffffff;
  --n-50: hsl(20 10% 97%);
  --n-100: hsl(20 10% 94%);
  --n-200: hsl(20 10% 88%);
  --n-300: hsl(20 10% 78%);
  --n-400: hsl(20 8% 60%);
  --n-500: hsl(20 8% 40%);
  --n-600: hsl(20 8% 30%);
  --n-700: hsl(20 10% 22%);
  --n-800: hsl(20 10% 14%);
  --n-850: hsl(20 10% 12%);
  --n-900: hsl(20 10% 10%);
  --n-950: hsl(20 12% 6%);

  /* Text on dark */
  --fg-dark: #ffffff;
  --fg-soft-dark: hsl(20 10% 86%);
  --muted-dark: hsl(20 6% 68%);
  --accent-text: var(--brand-400); /* headline phrase, step numbers, icons */
  --link-dark: var(--brand-300);   /* inline links, eyebrow, chevrons */

  /* Semantic tokens — dark set. .paper flips them (see Paper). */
  --bg: var(--n-950);
  --fg: var(--fg-dark);
  --fg-soft: var(--fg-soft-dark);
  --muted: var(--muted-dark);
  --tint: var(--n-900);
  --border: hsl(0 0% 100% / 0.12);          /* the hairline */
  --hairline-strong: hsl(0 0% 100% / 0.22);
  --link: var(--link-dark);
  --link-hover: var(--brand-200);
  --focus: var(--brand-300);

  /* Surfaces */
  --surface-1: var(--n-950); /* page */
  --surface-2: var(--n-900); /* tint bands, cards on the page */
  --surface-3: var(--n-850); /* cards inside tint bands, mobile nav */
  --surface-4: var(--n-800); /* hover state of surface-3, phone shell */

  /* Glass (hero cards, feature cards; the header uses its own alpha/blur). The hairline
     is a 1px gradient painted on the border-box behind a padding-box fill (§9.8). */
  --glass-fill: hsl(20 12% 6% / 0.62);
  --glass-blur: blur(16px) saturate(120%);
  --glass-hairline: linear-gradient(
    155deg,
    hsl(0 0% 100% / 0.3),
    hsl(0 0% 100% / 0.06) 40%,
    hsl(22 82% 60% / 0.1)
  );
  --glass-hairline-hover: linear-gradient(
    155deg,
    hsl(0 0% 100% / 0.45),
    hsl(0 0% 100% / 0.06) 40%,
    hsl(22 82% 60% / 0.22)
  );
  /* Orange floor glow under the phones (inside .phone-panel / .phone-stage / .phone-fan
     only — never under text). */
  --glow: radial-gradient(
    ellipse at 50% 100%,
    hsl(22 82% 46% / 0.55),
    hsl(22 82% 46% / 0.18) 40%,
    transparent 72%
  );

  /* Type */
  --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --t-eyebrow: 0.8125rem;
  --t-xs: 0.8125rem;
  --t-sm: 0.9375rem;
  --t-base: 1.0625rem;
  --t-lead: clamp(1.125rem, 1.05rem + 0.4vw, 1.375rem);
  --t-lg: var(--t-lead);
  --t-xl: clamp(1.1875rem, 1.1rem + 0.45vw, 1.375rem);
  --t-2xl: clamp(1.375rem, 1.25rem + 0.6vw, 1.75rem);
  --t-3xl: clamp(1.75rem, 1.35rem + 1.8vw, 2.75rem);
  --t-4xl: clamp(2.25rem, 1.5rem + 3vw, 3.75rem);
  --t-hero: clamp(2.5rem, 1.6rem + 4.2vw, 5rem);
  --lh-tight: 1.1;
  --lh-snug: 1.3;
  --lh-body: 1.6;

  /* 8px spacing scale */
  --s-1: 0.5rem;
  --s-2: 1rem;
  --s-3: 1.5rem;
  --s-4: 2rem;
  --s-5: 2.5rem;
  --s-6: 3rem;
  --s-8: 4rem;
  --s-10: 5rem;
  --s-12: 6rem;
  --s-16: 8rem;

  --radius: 8px;
  --radius-lg: 12px;
  --radius-xl: 24px;
  --radius-pill: 999px;
  --radius-frame: 28px; /* 20px on phones, see the ≤47.99rem override below the reset */

  /* Home hero frame: dark margin around the rounded photo card (§9.1) */
  --frame-inset: clamp(12px, 2vw, 28px);
  --frame-pad: clamp(1.25rem, 4vw, 3.5rem);

  --container: 72rem;
  /* §9.9 left spine: the x of a centred 72rem .container's content edge, expressed as a
     padding for boxes that are not .container (header row, hero copy). 100% resolves
     against the containing block, which for the header and the hero card is the frame's
     content box (viewport − 2 × --frame-inset); the container is centred in the viewport,
     so (100% − container)/2 + gutter measured from the frame edge lands on the container's
     content edge as long as the viewport is wider than 72rem + 2 × inset. Below that both
     collapse to the gutter. */
  --spine-pad: max(
    calc(var(--gutter) - var(--frame-inset)),
    calc((100% - var(--container)) / 2 + var(--gutter))
  );
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --section-y: clamp(3.5rem, 6vw + 1rem, 7rem);
  --header-h: 4.25rem;

  color-scheme: dark;
}

/* ---------- Reset ---------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}
@media (max-width: 47.99rem) {
  :root {
    --radius-frame: 20px;
  }
}
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: var(--s-4); /* the header is not sticky (§9.9); small breathing room */
  background: var(--n-950);
}
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--t-base);
  line-height: var(--lh-body);
  color: var(--fg);
  /* no background here on purpose: <html> above paints the same --n-950, and a background on
     <body> too would be a normal box painted AFTER the root's negative-z children — which is
     exactly where the page road lives (§9.1e). Two identical backgrounds, one of them hiding
     the road. Keep the colour on <html>. */
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-feature-settings: "cv11", "ss01";
}
img,
svg,
picture {
  display: block;
  max-width: 100%;
}
img {
  height: auto;
}
h1,
h2,
h3,
h4,
p,
ul,
ol,
figure,
dl,
dd {
  margin: 0;
}
ul,
ol {
  padding: 0;
}
a {
  color: var(--link);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}
a:hover {
  color: var(--link-hover);
}
/* The contact address is one unbreakable token; let it wrap inside narrow cards
   instead of forcing horizontal scroll at 320px. */
a[href^="mailto:"] {
  overflow-wrap: anywhere;
}
button {
  font: inherit;
  color: inherit;
}
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}
::selection {
  background: var(--brand-500);
  color: var(--n-950);
}

/* ---------- Typography ---------- */
h1,
h2,
h3,
h4 {
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: var(--lh-tight);
  color: var(--fg);
  text-wrap: balance;
}
h1 {
  font-size: var(--t-4xl);
  letter-spacing: -0.025em;
  line-height: 1.06;
}
h2 {
  font-size: var(--t-3xl);
  line-height: 1.12;
}
h3 {
  font-size: var(--t-xl);
  letter-spacing: -0.015em;
  line-height: var(--lh-snug);
}
h4 {
  font-size: var(--t-lg);
  letter-spacing: -0.01em;
  line-height: var(--lh-snug);
}
p {
  text-wrap: pretty;
}
.lead {
  font-size: var(--t-lead);
  line-height: 1.5;
  color: var(--fg-soft);
  max-width: 34em;
}
.eyebrow {
  display: block;
  font-size: var(--t-eyebrow);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--link-dark);
  margin-bottom: var(--s-2);
}
.muted {
  color: var(--muted);
}
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Layout ---------- */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container--narrow {
  /* F3: the narrow column sits on the shared left spine (the 72rem container's edge, where
     the wordmark is) and caps its measure instead of centring a 48rem box in the viewport. */
  max-width: var(--container);
}
.container--narrow > * {
  max-width: 48rem;
}
.container--narrow > .paper {
  max-width: 60rem;
}
.section {
  padding-block: var(--section-y);
}
.section--tight {
  padding-block: calc(var(--section-y) * 0.6);
}
.section--tint {
  background: var(--surface-2);
  border-block: 1px solid var(--border);
}
/* Adjacent tint bands must not double the hairline. */
.section--tint + .section--tint {
  border-top: 0;
}
/* Closing CTA band: the one warm moment on every page. */
.section--closing {
  /* the ground lives on a ::before at z-index -2, not on the section: the page road (§9.1e)
     is a z-index -1 child of the root, so a background on the section itself would paint over
     it and the road would stop at the pricing band instead of driving on into the closing
     card. Owner request 2026-09-03: "mangler vi ikke at den skal fortsætte ned i Del bilen
     med dem, du kender?" */
  position: relative;
  border-block: 1px solid var(--border);
}
.section--closing::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  border-radius: inherit;
  background: var(--surface-2)
    radial-gradient(48rem 20rem at 50% 0%, hsl(22 82% 46% / 0.18), transparent 70%);
}
.section--closing h2,
.section--closing h3 {
  color: var(--fg);
}
.section--closing p,
.section--closing .section-head p {
  color: var(--fg-soft);
}
.section-head {
  max-width: 38rem;
  margin-bottom: var(--s-6);
}
.section-head p {
  margin-top: var(--s-2);
  color: var(--fg-soft);
  font-size: var(--t-lead);
}
.section-head--center {
  margin-inline: auto;
  text-align: center;
}
/* A CTA button placed directly in a section head (opladning contact band). */
.section-head .btn {
  margin-top: var(--s-4);
}
.stack > * + * {
  margin-top: var(--s-2);
}
.stack--lg > * + * {
  margin-top: var(--s-4);
}

.grid-2,
.grid-3,
.grid-4 {
  display: grid;
  gap: var(--s-3) var(--s-3);
  /* minmax(0, 1fr), never bare 1fr: 1fr is minmax(auto, 1fr) and honours the
     min-content width of an unbreakable child (the mailto link), which pushed the
     column past the container at 320px. */
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 40rem) {
  .grid-2,
  .grid-3,
  .grid-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-4) var(--s-4);
  }
  /* Two cards of unequal length side by side (pricing): top-align instead of
     stretching the short one into a half-empty box. */
  .grid-2 {
    align-items: start;
  }
}
@media (min-width: 60rem) {
  .grid-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .grid-4 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Two-column editorial split: heading left, content right */
.split {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
@media (min-width: 56rem) {
  .split {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--s-8);
  }
}
/* ---------- Header ---------- */
.skip-link {
  position: absolute;
  left: var(--s-2);
  top: -100%;
  z-index: 100;
  padding: 0.6rem 1.1rem;
  background: var(--brand-500);
  color: var(--n-950);
  border-radius: var(--radius-pill);
  text-decoration: none;
  font-weight: 600;
  /* dark halo so the pill reads over the wordmark it covers */
  box-shadow: 0 0 0 4px var(--n-950), 0 8px 24px hsl(0 0% 0% / 0.5);
}
.skip-link:focus {
  top: var(--s-2);
  color: var(--n-950);
}

/* §9.9 Header inside the framed card. The header is absolutely positioned over the top
   of the first card on every page (photo hero or intro band): transparent, no blur bar,
   no hairline, not sticky. Its box is inset by --frame-inset exactly like the card, so
   the header's content edge and the card's content edge coincide. The first card pads
   its top by --header-h + spacing so nothing sits under the nav. */
.site-header {
  position: absolute;
  top: var(--frame-inset);
  left: var(--frame-inset);
  right: var(--frame-inset);
  z-index: 60;
  background: none;
}
.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  min-height: var(--header-h);
  position: relative;
}
/* F2: one left spine on every page. Below 64rem the row is a plain centred .container
   (which the hero copy and every section also use); from 64rem the row spans the card and
   pads its left to the spine (--spine-pad) while the nav pill keeps the frame's right edge. */
@media (min-width: 64rem) {
  .header-inner {
    max-width: none;
    padding-inline: var(--spine-pad) var(--frame-pad);
  }
  /* The .container inside the framed card (charging hero, intro pages) is inset by the
     frame, so a centred 72rem box would land 2*inset short of the header's spine below
     ~1230px. Pad it to the same spine instead of centring it. */
  .hero-frame > .container,
  .page-intro > .container {
    max-width: none;
    padding-inline: var(--spine-pad);
  }
}
.wordmark {
  font-weight: 600;
  font-size: 1.1875rem;
  letter-spacing: -0.01em;
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem; /* 44px tap target */
  gap: 0.6rem;
}
.wordmark::before {
  /* the brand mark, aftegnet udgave (mark.svg). Den tidligere regel — reduceret mærke
     under 64px — var målt på rekonstruktionen fra §10.1, ikke på aftegningen. Det
     aftegnede mærkes vej er bredere og holder ned til ca. 32px højde; derfor står det
     fulde mærke nu i headeren i 36px, hvor det faktisk ligner logoet. mark-sm.svg er
     stadig den rigtige under 32px (og ikonet under 24px) — se spec §10.2.
     Ratio er 1.7299 — hold boksen i den, ellers hælder vejen. Baggrundsbillede, så
     img-src 'self' dækker. ?v= er load-bearing: /assets/* er immutable i et år. */
  content: "";
  width: 3.894rem;
  height: 2.25rem;
  background: url("/assets/img/logo/mark.svg?v=4") center / contain no-repeat;
  flex: none;
}
.wordmark:hover {
  color: var(--fg);
}
.header-right {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
@media (min-width: 48rem) {
  /* wordmark left, nav links right, language pill last (A.png) */
  .site-nav {
    margin-inline-start: auto;
    margin-inline-end: var(--s-5);
  }
}
.site-nav ul {
  list-style: none;
  display: flex;
  gap: clamp(1.25rem, 2.5vw, 2.25rem);
  align-items: center;
}
.site-nav a {
  color: var(--fg-soft);
  text-decoration: none;
  font-weight: 500;
  font-size: var(--t-sm);
  padding: 0.65rem 0; /* 44px tall tap target with the 23px line */
  display: inline-block;
}
.site-nav a:hover {
  color: var(--fg);
}
.site-nav a[aria-current="page"] {
  color: var(--fg);
  background: linear-gradient(hsl(22 82% 50% / 0.6), hsl(22 82% 50% / 0.6)) no-repeat left bottom /
    60% 2px;
}
.lang-switch {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem; /* 44px tap target */
  font-size: var(--t-xs);
  font-weight: 500;
  color: var(--fg-soft);
  text-decoration: none;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  padding: 0.4rem 0.85rem;
  white-space: nowrap;
}
.lang-switch:hover {
  color: var(--fg);
  background: hsl(0 0% 100% / 0.08);
}
.nav-toggle {
  display: none;
  align-items: center;
  gap: 0.5rem;
  background: none;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  padding: 0.4rem 0.85rem;
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--fg-soft);
  cursor: pointer;
  min-height: 2.75rem; /* 44px tap target */
}
.nav-toggle:hover {
  color: var(--fg);
  background: hsl(0 0% 100% / 0.08);
}
.nav-toggle .bars {
  width: 1.125rem;
  height: 0.875rem;
  position: relative;
  display: inline-block;
  color: var(--fg);
}
.nav-toggle .bars::before,
.nav-toggle .bars::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: currentColor;
  border-radius: 1px;
  transition: transform 0.2s ease;
}
.nav-toggle .bars::before {
  top: 0;
  box-shadow: 0 6px 0 currentColor;
}
.nav-toggle .bars::after {
  bottom: 0;
}
.nav-toggle[aria-expanded="true"] .bars::before {
  transform: translateY(6px) rotate(45deg);
  box-shadow: none;
}
.nav-toggle[aria-expanded="true"] .bars::after {
  transform: translateY(-6px) rotate(-45deg);
}

@media (max-width: 47.99rem) {
  .nav-toggle {
    display: inline-flex;
  }
  /* The dropdown. Only display/position/border/padding/box-shadow live on the base
     class — those are exactly what the frozen <noscript> block resets. Background and
     radius sit on .is-open, which only site.js ever adds, so the no-JS render is flat. */
  .site-nav {
    display: none;
    position: absolute;
    left: var(--gutter);
    right: var(--gutter);
    top: calc(100% + 0.5rem);
    border: 1px solid var(--border);
    padding: var(--s-1) var(--s-3);
    z-index: 50;
  }
  .site-nav.is-open {
    display: block;
    background: hsl(20 12% 6% / 0.96);
    border-radius: var(--radius-lg);
    box-shadow: 0 24px 48px hsl(0 0% 0% / 0.5);
  }
  /* Without JS the <noscript> block makes the nav static, so it becomes a third header
     row inside the framed card (§9.9). Keep that row one compact line of small links so
     it stays above the card's top padding instead of running into the h1; the tall
     one-per-line list only applies to the JS dropdown (.is-open). */
  .site-nav ul {
    flex-wrap: wrap;
    gap: var(--s-1) var(--s-3);
  }
  .site-nav a {
    padding: 0.4rem 0;
    font-size: var(--t-xs);
    box-shadow: none;
  }
  .site-nav a[aria-current="page"] {
    color: var(--link-dark);
    box-shadow: none;
    background: none;
  }
  .site-nav.is-open ul {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }
  .site-nav.is-open a {
    display: block;
    padding: 0.85rem 0;
    font-size: var(--t-base);
    border-bottom: 1px solid var(--border);
  }
  .site-nav.is-open li:last-child a {
    border-bottom-color: transparent;
  }
  .header-inner {
    /* the absolute nav needs the header, not the container, as its box */
    position: static;
    gap: var(--s-2);
    min-width: 0;
    /* Without JS the <noscript> block makes the nav static; let it wrap onto its
       own row instead of forcing the header wider than 320px. With JS the nav is
       absolute and takes no flex space, so this is a no-op. */
    flex-wrap: wrap;
  }
  .site-nav {
    flex: 1 1 100%;
    order: 3;
  }
  /* Absolute (not relative) so the header sits over the card on phones too; it is
     still the containing block for the absolutely positioned dropdown. */
  .site-header {
    position: absolute;
  }
  .wordmark {
    font-size: 1.0625rem;
  }
}
@media (max-width: 30rem) {
  /* Narrow phones: keep the menu label for assistive tech, drop it visually */
  .nav-toggle-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .nav-toggle {
    padding: 0.4rem 0.6rem;
    min-width: 2.75rem; /* 44px tap target */
    justify-content: center;
  }
  .lang-switch {
    padding: 0.4rem 0.7rem;
  }
}
@media (max-width: 23rem) {
  /* 320px-class viewports (and 400% zoom on a 1280px screen): the header row is
     wordmark + language switch + menu button and must fit one row inside the
     280px content box (wordmark ~150px + 8px + header-right ~117px), otherwise the
     wrap above pushes the buttons onto a second row and doubles the header height. */
  .header-inner {
    gap: var(--s-1);
    /* §9.9: the header is inset by the frame (12px) as well, so the content box is
       256px at the page gutter; pull the header's own padding in to 12px so the row
       (wordmark ~150 + 8 + pill ~60 + 4 + button 44) still fits on one line. */
    padding-inline: 0.75rem;
  }
  .header-right {
    gap: 0.25rem;
  }
  .lang-switch {
    padding: 0.35rem 0.55rem;
  }
  .wordmark {
    font-size: 0.875rem;
    gap: 0.4rem;
  }
}

/* Windows High Contrast / forced colours strip background and box-shadow, which is
   how the three hamburger bars are drawn; redraw them in the system button colour. */
@media (forced-colors: active) {
  .nav-toggle .bars,
  .nav-toggle .bars::before,
  .nav-toggle .bars::after {
    forced-color-adjust: none;
  }
  .nav-toggle .bars::before,
  .nav-toggle .bars::after {
    background: ButtonText;
  }
  .nav-toggle .bars::before {
    box-shadow: 0 6px 0 ButtonText;
  }
  .nav-toggle[aria-expanded="true"] .bars::before {
    box-shadow: none;
  }
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding: 0.8rem 1.4rem;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  font-weight: 600;
  font-size: var(--t-sm);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
/* Orange fill with near-black text (4.92:1). White on brand-500 fails AA at button size. */
.btn-primary {
  background: var(--brand-500);
  border-color: var(--brand-500);
  color: var(--n-950);
}
.btn-primary:hover {
  background: var(--brand-400);
  border-color: var(--brand-400);
  color: var(--n-950);
}
.btn-secondary {
  background: transparent;
  border-color: var(--hairline-strong);
  color: var(--fg);
}
.btn-secondary:hover {
  background: hsl(0 0% 100% / 0.08);
  border-color: var(--hairline-strong);
  color: var(--fg);
}
.btn-lg {
  min-height: 3.25rem;
  padding: 0.95rem 1.7rem;
  font-size: var(--t-base);
}
.store-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}
.store-badges--center {
  justify-content: center;
}
.store-badges img {
  height: 3.25rem;
  width: auto;
}
@media (max-width: 26rem) {
  .store-badges .btn {
    flex: 1 1 100%;
  }
}
/* Store badges on dark: the black Apple/Google artwork is made for light surfaces, so
   EACH badge sits on its own white plate (two buttons, not one slab). Never recolour or
   redraw the badges themselves. Apply only where the children are badge <img>s, never
   to text .btn groups. */
.store-badges--paper {
  gap: var(--s-3);
}
.store-badges--paper a {
  display: inline-flex;
  background: var(--n-0);
  border-radius: 14px;
  padding: 0.5rem 0.75rem;
  transition: opacity 0.15s ease;
}
.store-badges--paper a:hover {
  opacity: 0.82;
}
.store-badges--paper a:focus-visible {
  /* 7.64:1 on white, drawn inset on the plate's own padding (8px/12px), never over the
     artwork and never outside on n-950 */
  outline: 3px solid var(--brand-700);
  outline-offset: -3px;
}

/* ---------- Cards ---------- */
.card {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: clamp(1.25rem, 3vw, 2rem);
}
@media (max-width: 22rem) {
  .card {
    padding: 1rem;
  }
}
.section--tint .card {
  background: var(--surface-3);
}
.card h3,
.card h4 {
  margin-bottom: var(--s-1);
}
.card p {
  color: var(--fg-soft);
}
.card--accent {
  border-top: 3px solid var(--brand-500);
}
.card--tint {
  background: var(--surface-3);
  border-color: transparent;
}

/* ---------- Hero (photo) ---------- */
/* Home = courtyard photo; /opladning + /en/charging = charging photo (.page-charging).
   The photo is an <img> inside <picture class="hero-media">, never a background-image.
   Below 64rem the photo is a top band that fades into flat n-950; the copy starts in the
   band's lower third and everything after the h1 sits on solid n-950. */
.hero--photo {
  position: relative;
  isolation: isolate;
  background: var(--n-950); /* placeholder while the photo loads; no LQIP */
  overflow: hidden;
  /* §9.9: the band is itself the framed card — inset from the viewport, rounded, with a
     hairline — and the absolute header sits over its top (the header's own box is inset
     by the same --frame-inset). */
  margin: var(--frame-inset) var(--frame-inset) 0;
  border-radius: var(--radius-frame);
  box-shadow: inset 0 0 0 1px hsl(0 0% 100% / 0.1);
  padding-block: 0 var(--s-8);
}
.hero-media {
  position: absolute;
  inset: 0;
  bottom: auto;
  height: min(60svh, 34rem);
  width: 100%;
  z-index: -2;
}
.hero-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 40%;
}
.hero--photo::before {
  content: "";
  position: absolute;
  inset: 0;
  bottom: auto;
  height: min(60svh, 34rem);
  z-index: -1;
  background:
    linear-gradient(to bottom, hsl(20 12% 6% / 0.55), transparent 30%), /* nav legibility */
    linear-gradient(
      to bottom,
      hsl(20 12% 6% / 0.25) 0,
      hsl(20 12% 6% / 0.55) 45%,
      hsl(20 12% 6% / 0.92) 82%,
      hsl(20 12% 6%) 100%
    );
}
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-8);
}
.hero-copy {
  display: grid;
  gap: var(--s-4);
  align-content: end;
  padding-top: min(34svh, 19rem); /* h1 starts in the band's lower third */
}
.hero-copy > * {
  min-inline-size: 0;
}
.hero--photo h1 {
  font-size: var(--t-hero);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.02;
  max-width: 14ch;
  color: var(--fg);
}
.h1-accent {
  color: var(--accent-text);
}
/* Over the photo brand-400 dips under 3:1 where the scrim thins; brand-300 measured
   >= 3.6:1 everywhere the headline tail can land. */
.hero--photo .h1-accent {
  color: var(--brand-300);
}
.hero--photo .lead {
  max-width: 38ch;
}
.hero--photo .store-badges {
  justify-self: start;
}

/* Hero cards: three links, one per subpage. Glass at rest, hairline brightens on hover,
   the title underlines, the card lifts 2px. The circled chevron is the same "this is a
   control" primitive as the FAQ ± — feature cards never get one. */
.hero-cards {
  list-style: none;
  display: grid;
  gap: var(--s-3);
}
/* Glass v2 (§9.8): semi-opaque fill on the padding-box, a 1px gradient hairline on the
   border-box behind it (the border itself is transparent), backdrop blur + saturate, and a
   1px inner top highlight. Blur only where something is behind the card (hero over photo);
   flat surfaces use .glass--still. */
.glass {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--glass-fill), var(--glass-fill)) padding-box,
    var(--glass-hairline) border-box;
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow:
    inset 0 1px 0 hsl(0 0% 100% / 0.1),
    0 24px 48px -16px hsl(0 0% 0% / 0.45);
}
@supports not (backdrop-filter: blur(1px)) {
  .glass {
    background:
      linear-gradient(hsl(20 10% 10% / 0.92), hsl(20 10% 10% / 0.92)) padding-box,
      var(--glass-hairline) border-box;
  }
}
/* Declared AFTER the @supports block on purpose: both are single-class and both set
   background, so no-backdrop browsers would otherwise paint the .92 fallback over this. */
.glass--still {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background:
    linear-gradient(hsl(20 10% 12% / 0.85), hsl(20 10% 9% / 0.85)) padding-box,
    var(--glass-hairline) border-box;
}
.hero-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--s-3);
  row-gap: var(--s-1);
  align-items: center;
  padding: clamp(1.25rem, 3.5vw, 1.75rem);
  border-radius: var(--radius-xl);
  color: var(--fg);
  text-decoration: none;
  min-inline-size: 0;
  transition: border-color 0.2s ease, transform 0.2s ease;
}
.hero-card-title {
  grid-column: 1;
  font-weight: 600;
  font-size: var(--t-xl);
  line-height: var(--lh-snug);
  letter-spacing: -0.015em;
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
}
.hero-card-chevron {
  grid-column: 2;
  grid-row: 1;
  align-self: center;
  display: inline-grid;
  place-items: center;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--fg-soft);
  flex: none;
  transition: transform 0.2s ease, color 0.2s ease;
}
.hero-card:hover .hero-card-chevron,
.hero-card:focus-visible .hero-card-chevron {
  color: var(--fg);
  transform: translateX(2px);
}
.hero-card-chevron svg {
  width: 1rem;
  height: 1rem;
}
.hero-card:hover,
.hero-card:focus-visible {
  /* the hairline's first stop lifts to .45 (gradients cannot transition, so this snaps) */
  background:
    linear-gradient(var(--glass-fill), var(--glass-fill)) padding-box,
    var(--glass-hairline-hover) border-box;
  color: var(--fg);
  transform: translateY(-2px);
}
.hero-card:hover .hero-card-title,
.hero-card:focus-visible .hero-card-title {
  text-decoration: underline;
}
/* The ring must read over the photo, not just over the scrim: a brand-300 ring drawn
   on an n-950 halo that extends past it on both sides (outline paints above box-shadow),
   so the ring's adjacent colour is always n-950 (9.3:1) whatever pixel the photo has. */
.hero-card:focus-visible {
  outline: 2px solid var(--brand-300);
  outline-offset: 3px;
  box-shadow: 0 0 0 7px var(--n-950);
}

@media (min-width: 64rem) {
  .hero--photo {
    min-height: min(88vh, 52rem);
    min-height: min(88svh, 52rem);
    display: grid;
    align-items: center;
    padding-block: calc(var(--header-h) + var(--s-10)) var(--s-10);
  }
  .hero-media,
  .hero--photo::before {
    inset: 0;
    height: 100%;
  }
  .hero-media img {
    /* cover is width-bound here, so only the vertical position moves: 60% keeps the
       wheels and the wet-ground reflection in frame at 1920 (832px band). */
    object-position: 50% 60%;
  }
  .hero--photo::before {
    background:
      linear-gradient(to bottom, hsl(20 12% 6% / 0.55), transparent 30%),
      linear-gradient(
        90deg,
        hsl(20 12% 6% / 0.84) 0%,
        hsl(20 12% 6% / 0.74) 40%,
        hsl(20 12% 6% / 0.3) 66%,
        hsl(20 12% 6% / 0.3) 100%
      ),
      linear-gradient(to top, hsl(20 12% 6%) 0%, hsl(20 12% 6% / 0) 28%);
  }
  .hero-grid {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    align-items: center;
    gap: var(--s-8);
  }
  .hero-copy {
    gap: var(--s-5);
    padding-top: 0;
    align-content: center;
  }
  .hero-cards {
    justify-self: end;
    width: min(100%, 20rem);
  }
  .hero-card {
    padding: clamp(1.125rem, 3vw, 1.5rem);
  }
  /* Keep the accent phrase on one line where the copy column is wide enough. */
  .h1-accent {
    display: inline-block;
  }
}

/* Charging pages: a subpage title over a photo band, not a proposition. Smaller hero,
   single column at every width, h1 at the subpage size. No cards, no badges. */
.page-charging .hero-media img {
  object-position: 55% 45%;
}
:is(.page-charging, .page-press) .hero-grid {
  grid-template-columns: minmax(0, 1fr);
}
:is(.page-charging, .page-press) .hero--photo h1 {
  font-size: var(--t-4xl);
  letter-spacing: -0.025em;
  line-height: 1.06;
}
@media (min-width: 64rem) {
  :is(.page-charging, .page-press) .hero--photo {
    min-height: min(62vh, 34rem);
    min-height: min(62svh, 34rem);
    padding-block: calc(var(--header-h) + var(--s-8)) var(--s-8);
  }
  .page-charging .hero-media img {
    /* 1280: the scaled photo is 731px tall in a 544px band; 88% shows the whole car,
       roof to wheels. */
    object-position: 65% 88%;
  }
}
@media (min-width: 90rem) {
  .page-charging .hero-media img {
    /* 1920: the photo is 1097px tall, the car alone 610px, so something has to go;
       keep the roof and body, lose the lower half of the wheels. */
    object-position: 65% 62%;
  }
}

/* ---------- Phone frame ---------- */
/* Only ever used with the real 03-bookings screenshot. Four placements on the home page
   (§9): upright in .phone-panel, tilted in .phone-stage, two in .phone-fan — never two in
   one section. */
.phone {
  position: relative;
  width: min(17.5rem, 100%);
  aspect-ratio: 9 / 19.5;
  background: var(--surface-4);
  border: 1px solid var(--hairline-strong);
  border-radius: 2.75rem;
  padding: 0.7rem;
  box-shadow: 0 32px 64px hsl(0 0% 0% / 0.55);
  flex: none;
}
.phone::before {
  /* front camera island */
  content: "";
  position: absolute;
  top: 1.35rem;
  left: 50%;
  transform: translateX(-50%);
  width: 5.25rem;
  height: 1.3rem;
  border-radius: 999px;
  background: var(--surface-4);
  z-index: 1;
}
.phone img,
.phone picture {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  border-radius: 2.1rem;
  background: var(--n-900);
  overflow: hidden;
  color: var(--fg-soft); /* alt text colour if the image is missing */
  font-size: var(--t-sm);
  text-align: center;
  padding-top: 0;
}

/* ---------- Steps (how it works) ---------- */
.steps {
  list-style: none;
  counter-reset: step;
}
.step {
  counter-increment: step;
  border-top: 1px solid var(--border);
  padding-top: var(--s-4);
}
.step::before {
  content: counter(step, decimal-leading-zero);
  display: block;
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--accent-text);
  margin-bottom: var(--s-2);
  font-variant-numeric: tabular-nums;
}
.step h3 {
  font-size: var(--t-xl);
  margin-bottom: var(--s-1);
}
.step p {
  color: var(--fg-soft);
  font-size: var(--t-sm);
}

/* ---------- Feature list ---------- */
.feature h3 {
  font-size: var(--t-xl);
  color: var(--fg);
  margin-bottom: 0.35rem;
}
.feature p {
  color: var(--fg-soft);
  font-size: var(--t-sm);
}

/* ---------- Trust row ---------- */
.trust-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-2);
  align-items: start;
}
.trust-item svg {
  width: 1.75rem;
  height: 1.75rem;
  color: var(--accent-text);
  margin-top: 0.15rem;
}
.trust-item h3 {
  font-size: var(--t-xl);
  color: var(--fg);
  margin-bottom: 0.25rem;
}
.trust-item p {
  color: var(--fg-soft);
}

/* ---------- Pricing teaser / price cards ---------- */
.price-card {
  display: grid;
  gap: var(--s-3);
}
.price-card .btn {
  justify-self: start;
}
.price-card h3 {
  font-size: var(--t-3xl);
  color: var(--fg);
  letter-spacing: -0.02em;
  line-height: 1.12;
}
@media (min-width: 40rem) {
  .price-card {
    padding: var(--s-5);
  }
}

/* ---------- FAQ ---------- */
.faq {
  border-top: 1px solid var(--border);
  /* Cap the row so the +/- toggle stays within reach of the question text; the
     narrow container is 43rem of content at 1280, so the cap has to sit below it. */
  max-width: 38rem;
}
.faq details {
  border-bottom: 1px solid var(--border);
}
.faq summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
  padding-block: var(--s-3);
  font-weight: 600;
  font-size: var(--t-base);
  color: var(--fg);
  letter-spacing: -0.005em;
}
.faq summary::-webkit-details-marker {
  display: none;
}
.faq summary::after {
  /* The open/closed state is already conveyed by <details>; the second declaration
     (alternative-text syntax) keeps screen readers from announcing "plus" / "en dash".
     Browsers without the slash syntax drop it and keep the first. */
  content: "+";
  content: "+" / "";
  flex: none;
  width: 1.75rem;
  height: 1.75rem;
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  font-weight: 400;
  color: var(--link-dark);
  line-height: 1;
  font-size: 1.25rem;
}
.faq details[open] summary::after {
  content: "–";
  content: "–" / "";
}
.faq summary:hover {
  color: var(--brand-200);
}
.faq details > div,
.faq details > p {
  padding-bottom: 1.25rem;
  color: var(--fg-soft);
  max-width: 60ch;
}
.faq details > div > * + * {
  margin-top: var(--s-2);
}

/* ---------- Paper (light surface for legal prose) ---------- */
/* Re-scopes the semantic tokens to the light set, so the .prose / table / .meta /
   .btn-secondary rules below render correctly inside it without their own variants. */
.paper {
  --bg: var(--n-0);
  --fg: var(--n-900);
  --fg-soft: var(--n-700);
  --muted: var(--n-500);
  --tint: var(--n-50);
  --border: var(--n-200);
  --hairline-strong: var(--n-300);
  --link: var(--brand-700);
  --link-hover: var(--brand-800);
  --focus: var(--brand-700);
  color-scheme: light;
  background: var(--bg);
  color: var(--fg);
  /* §9.10: the paper matches the framed intro card above it — same radius, a 1px ring
     (a shadow, not a border: --border is re-scoped to n-200 in here) and a soft drop
     shadow so the light sheet floats over the dark ground. */
  border-radius: var(--radius-frame);
  box-shadow:
    0 0 0 1px hsl(0 0% 100% / 0.12),
    0 32px 64px -24px hsl(0 0% 0% / 0.6);
  padding: clamp(1.25rem, 4vw, 3rem);
}
.paper ::selection {
  background: var(--brand-100);
  color: var(--brand-950);
}
.paper .btn-secondary {
  border-color: var(--n-300);
  color: var(--n-900);
}
.paper .btn-secondary:hover {
  background: var(--n-100);
  border-color: var(--n-500);
  color: var(--n-900);
}
@media (max-width: 30rem) {
  /* Collapse the double inset (container gutter + paper padding) so the prose keeps a
     readable measure at 320px, and let tables use the full paper width before scrolling. */
  .paper {
    padding-inline: var(--s-2);
    border-radius: var(--radius-lg);
  }
  .paper .table-wrap {
    margin-inline: calc(var(--s-2) * -1);
    padding-inline: var(--s-2);
  }
}

/* ---------- Prose (legal & long-form pages) ---------- */
.prose {
  max-width: 68ch;
  line-height: 1.65;
}
.prose > * + * {
  margin-top: 1.1em;
}
.prose h1 {
  font-size: var(--t-3xl);
}
.prose h2 {
  font-size: var(--t-2xl);
  margin-top: 2.25em;
}
.prose h3 {
  font-size: var(--t-xl);
  margin-top: 1.75em;
}
.prose h4 {
  font-size: var(--t-base);
  font-weight: 600;
  margin-top: 1.5em;
}
.prose h2 + *,
.prose h3 + *,
.prose h4 + * {
  margin-top: 0.6em;
}
.prose p,
.prose li {
  color: var(--fg-soft);
}
.prose strong {
  color: var(--fg);
  font-weight: 600;
}
.prose a,
.prose td {
  overflow-wrap: anywhere;
}
.prose ul,
.prose ol {
  padding-left: 1.4em;
}
.prose li + li {
  margin-top: 0.4em;
}
.prose ul {
  list-style: disc;
}
.prose ol {
  list-style: decimal;
}
.prose hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin-block: 2.5em;
}
.prose blockquote {
  margin: 1.5em 0;
  padding-left: 1.25em;
  border-left: 3px solid var(--brand-300);
  color: var(--fg-soft);
}
.prose .table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
/* Below ~26rem the columns squeeze to one word per line; let the sheet scroll instead. */
.paper .table-wrap table {
  min-width: 26rem;
}
.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-sm);
}
.prose th,
.prose td {
  text-align: left;
  vertical-align: top;
  padding: 0.7rem 0.85rem;
  border-bottom: 1px solid var(--border);
}
.prose th {
  font-weight: 600;
  color: var(--fg);
  background: var(--tint);
}
.prose dl dt {
  font-weight: 600;
  color: var(--fg);
}
.prose dl dd {
  margin: 0.25em 0 1em;
  color: var(--fg-soft);
}
.prose .meta {
  font-size: var(--t-sm);
  color: var(--muted);
}
/* Table of contents on the legal pages: each link a 44px-tall tap target. display:block
   (not inline-block) so the list marker aligns with the FIRST line of a wrapped item. */
.prose nav ol a {
  display: block;
  padding-block: 0.55rem; /* 44px tall tap target with the 27px line */
}
.prose nav li + li {
  margin-top: 0;
}

/* ---------- Page intro (typographic hero for subpages) ---------- */
/* n-950 with a soft brand glow top-left (the same recipe as .section--closing, anchored
   at 20% so it sits behind the eyebrow + h1), hairline bottom. */
.page-intro {
  background: var(--surface-1)
    radial-gradient(60rem 24rem at 20% 0%, hsl(22 82% 46% / 0.18), transparent 70%);
  /* §9.9: the intro band is the framed card on pages without a photo; the absolute
     header sits in its top --header-h, hence the tall top padding. */
  margin: var(--frame-inset) var(--frame-inset) 0;
  border-radius: var(--radius-frame);
  border: 1px solid hsl(0 0% 100% / 0.1);
  padding-block: calc(var(--header-h) + var(--s-6)) var(--s-8);
}
@media (min-width: 64rem) {
  .page-intro {
    padding-block: calc(var(--header-h) + var(--s-8)) var(--s-8);
  }
  /* F11: the page title is the one display-size element on an intro page; the section h2s
     below stay at --t-4xl so the two levels are a clear step apart. */
  .page-intro h1 {
    font-size: var(--t-hero);
  }
}
/* Support intro: the mailto in the lead must not break mid-word on narrow phones.
   The address is 31 unbreakable characters; inside the card it has 246px at 320. */
@media (max-width: 26rem) {
  .page-intro .card .lead {
    font-size: var(--t-base);
  }
}
@media (max-width: 22rem) {
  .page-intro .card .lead {
    font-size: 0.875rem;
  }
}
.page-intro h1 {
  font-size: var(--t-4xl);
}
.page-intro .lead {
  margin-top: var(--s-3);
}
/* Support: the contact card follows the h1 directly. */
.page-intro h1 + .card {
  margin-top: var(--s-4);
}
/* A page-intro followed directly by a tint band: one hairline, not two, and a strip of
   dark ground under the rounded card (§9.10, same as under the charging photo card). */
.page-intro + .section--tint {
  border-top: 0;
  margin-top: var(--frame-inset);
}
/* F9 404: both language blocks live inside the framed intro card as one column; the
   English block is separated by a hairline and set at the same sizes as the Danish one. */
.page-404 .page-intro h2 {
  font-size: var(--t-4xl);
  line-height: var(--lh-tight);
  letter-spacing: -0.02em;
}
.page-404 .page-intro .stack + .stack {
  margin-top: var(--s-6);
  padding-top: var(--s-6);
  border-top: 1px solid var(--border);
}
.page-404 .page-intro .btn {
  margin-top: var(--s-4);
}
@media (min-width: 64rem) {
  .page-404 .page-intro h2 {
    font-size: var(--t-hero);
  }
}

/* ---------- Footer ---------- */
.site-footer {
  background: var(--surface-1);
  border-top: 1px solid var(--border);
  padding-block: var(--s-8) var(--s-6);
  font-size: var(--t-sm);
  color: var(--fg-soft);
}
.footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-6);
  align-items: start;
}
@media (min-width: 40rem) {
  .footer-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (min-width: 60rem) {
  .footer-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 3fr) minmax(0, 4fr);
    gap: var(--s-8);
  }
  /* Column 3 opens with the "CommuteConnects" eyebrow (frozen copy, no counterpart in
     column 2); pad the nav column by exactly that block so both link lists start on the
     same line. */
  .footer-grid > nav {
    padding-top: calc(var(--t-eyebrow) * var(--lh-body) + var(--s-2));
  }
}
.footer-brand p {
  margin-top: var(--s-2);
  max-width: 34em;
  color: var(--muted);
}
.footer-heading {
  font-size: var(--t-eyebrow);
  font-weight: 600;
  line-height: var(--lh-body);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--s-2);
}
.footer-links {
  list-style: none;
}
.footer-links li + li {
  margin-top: 0;
}
.footer-links a {
  display: inline-block;
  min-width: 2.75rem;
  padding-block: 0.625rem; /* 44px tall tap target with the 24px line */
  color: var(--fg-soft);
  text-decoration: none;
}
.footer-links a:hover {
  color: var(--fg);
  text-decoration: underline;
}
.footer-legal {
  margin-top: var(--s-6);
  padding-top: var(--s-3);
  border-top: 1px solid var(--border);
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: 1.7;
}
.footer-legal a {
  color: var(--fg-soft);
}

/* ======================================================================================
   §9 Mockup fidelity. Every rule below is an opt-in class. The home page uses all of them;
   the charging pages opt into the hero frame only (§9.10), the other subpages take the
   glass/hairline treatment on their cards, FAQ and paper (§9.10).
   ====================================================================================== */

/* ---------- 9.1 Hero frame: the photo inside a rounded card with a dark margin ---------- */
/* .hero--frame sits on the same <section> as .hero--photo and zeroes its header pull and
   band heights; the frame (.hero-frame) is the rounded clip, the photo covers it. */
.hero--frame {
  --t-hero: clamp(2.25rem, 1.5rem + 3vw, 4rem);
  margin: 0;
  border-radius: 0;
  box-shadow: none;
  padding: var(--frame-inset) var(--frame-inset) 0;
  background: var(--n-950);
  overflow: visible;
  min-height: 0;
  display: block;
}
.hero--frame::before {
  content: none; /* the scrim moves onto .hero-frame::before */
}
.hero-frame {
  position: relative;
  isolation: isolate;
  /* clip, not hidden: a scroll container would lose the content-based minimum height and
     the copy could be cut off on short viewports. */
  overflow: hidden;
  overflow: clip;
  border-radius: var(--radius-frame);
  background: var(--n-950);
  display: grid;
  align-items: end;
  /* 14rem top: room for the header (--header-h + its 1rem inset) plus the photo's face
     before the copy. The card's height is set by its content on phones/tablets (the photo
     stays visible behind the cards) and by the fold on desktop (see ≥64rem). */
  padding: 14rem var(--gutter) var(--frame-pad);
}
/* The card edge. A z-index:2 overlay, not an inset box-shadow on the frame: the photo and
   the scrim are negative-z children of the isolated frame and would paint over a shadow. */
.hero-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px hsl(0 0% 100% / 0.12);
  z-index: 2;
  pointer-events: none;
}
/* Header inside the framed card (every page, §9.9): 1rem of air above the wordmark; no
   active-page underline inside the card (A.png has none) — the current page is white and
   semibold. Same specificity as the base rules, declared later, so these win. */
.site-header {
  padding-top: var(--s-2);
}
.site-nav a[aria-current="page"] {
  background: none;
  color: var(--fg);
  font-weight: 600;
}
/* The language pill sits over the photo (a lit window at 1920) or the intro glow: a
   translucent dark fill under the hairline keeps it legible whatever is behind it. */
.lang-switch {
  background: hsl(20 12% 6% / 0.35);
}
.lang-switch:hover {
  background: hsl(20 12% 6% / 0.55);
  color: var(--fg);
}
.hero--frame .hero-media,
.hero-frame::before {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
}
.hero--frame .hero-media {
  z-index: -2;
}
.hero--frame .hero-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 70% 40%;
  filter: saturate(1.05);
}
.hero-frame::before {
  content: "";
  z-index: -1;
  /* §1 mobile scrim: fixed lengths, so the copy (which starts at 14rem) always sits on ≥.78;
     the tail is capped at .90 so the photo still shows behind the stacked cards. The first
     layer darkens the top band behind the nav (§9.9). */
  background:
    linear-gradient(to bottom, hsl(20 12% 6% / 0.65), transparent 30%),
    linear-gradient(
      to bottom,
      hsl(20 12% 6% / 0.3) 0,
      hsl(20 12% 6% / 0.78) 16rem,
      hsl(20 12% 6% / 0.9) 100%
    );
}
@media (max-width: 47.99rem) {
  /* F5 (home only): object-position cannot lift the car — it sits in the lower half of the
     portrait crop — so the photo's box changes instead: a band wider than the card, 3:4,
     pulled up by ~28 % of its own height so the car lands directly under the header, and
     masked into the ground before the headline. The copy starts at 20rem. */
  .page-home .hero-frame {
    padding-top: 20rem;
  }
  .page-home .hero--frame .hero-media {
    inset: 0 auto auto -30%;
    width: 130%;
    max-width: none; /* the picture reset caps at 100 % */
    height: auto;
    aspect-ratio: 3 / 4;
    transform: translateY(-28%);
    -webkit-mask-image: linear-gradient(to bottom, #000 58%, transparent 96%);
    mask-image: linear-gradient(to bottom, #000 58%, transparent 96%);
  }
  .page-home .hero--frame .hero-media img {
    object-position: 50% 50%;
  }
  /* the lightest scrim stop sits over the car (to 11rem); solid n-950 from ~26rem where
     the band has faded out, so the copy and cards stand on the ground, not on the photo */
  .page-home .hero-frame::before {
    background:
      linear-gradient(
        to bottom,
        hsl(20 12% 6% / 0.82) 0,
        hsl(20 12% 6% / 0.82) 5.5rem,
        transparent 9.5rem
      ),
      linear-gradient(
        to bottom,
        hsl(20 12% 6% / 0.2) 0,
        hsl(20 12% 6% / 0.2) 11rem,
        hsl(20 12% 6% / 0.8) 18rem,
        hsl(20 12% 6%) 26rem
      );
  }
}
.hero--frame .hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-6);
}
.hero--frame .hero-copy {
  padding-top: 0;
  gap: var(--s-4);
  align-content: end;
}
.hero--frame .hero-cards {
  margin-top: var(--s-2);
  min-inline-size: 0;
}
.hero--frame h1 {
  max-width: 18ch;
}
@media (max-width: 22rem) {
  /* 320: the accent phrase must not break mid-phrase — let the headline use the full
     card width at a size that fits "Én bil. Flere naboer." on two lines. */
  .hero--frame h1 {
    max-width: none;
    font-size: 1.875rem;
    text-wrap: balance;
  }
  /* "Flere naboer." fits the 256px content box at 2rem; the English phrase does not, so
     only Danish pins it to one line — the balanced wrap does the rest. */
  .hero--frame .h1-accent:lang(da) {
    white-space: nowrap;
  }
}
/* Store badges on one row from 24rem (two 40px plates + gap fit in the 326px content box
   at 390); below that they may wrap. */
@media (min-width: 24rem) {
  .hero--frame .store-badges {
    flex-wrap: nowrap;
  }
  .hero--frame .store-badges img {
    height: 2.5rem;
  }
}
@media (min-width: 64rem) {
  .hero--frame {
    min-height: 0;
    display: block;
    padding: var(--frame-inset) var(--frame-inset) 0;
  }
  .hero-frame {
    /* the card meets the fold: viewport minus the dark margin above and below (A.png).
       No max-height — on a short viewport the content sets the height instead of being
       clipped. */
    min-height: calc(100svh - 2 * var(--frame-inset));
    align-items: center;
    /* the header lives in the top --header-h (+1rem) of the card; centre the copy below it.
       F2: the copy's left edge is the shared spine, the right edge the frame pad. */
    padding: calc(var(--header-h) + var(--s-2) + var(--frame-pad)) var(--frame-pad)
      var(--frame-pad) var(--spine-pad);
  }
  .hero--frame .hero-media img {
    object-position: 50% 60%;
  }
  .wordmark {
    font-size: 1.25rem;
    letter-spacing: -0.01em;
  }
  .site-nav ul {
    gap: clamp(1.5rem, 2.8vw, 3.5rem);
  }
  .hero-frame::before {
    /* §1 desktop scrim: copy zone ≥.60 to 38 % (measured ≥3:1 under .h1-accent), tail ≥.30,
       bottom fade to n-950; first layer = top band behind the nav (§9.9) */
    background:
      linear-gradient(to bottom, hsl(20 12% 6% / 0.65), transparent 30%),
      linear-gradient(
        90deg,
        hsl(20 12% 6% / 0.72) 0%,
        hsl(20 12% 6% / 0.66) 38%,
        hsl(20 12% 6% / 0.3) 66%,
        hsl(20 12% 6% / 0.3) 100%
      ),
      linear-gradient(to top, hsl(20 12% 6% / 0.72) 0%, hsl(20 12% 6% / 0) 28%);
  }
  .hero--frame .hero-grid {
    /* copy ≤45 % of the card, cards pushed to the far right (off the car's nose) */
    grid-template-columns: minmax(0, 45%) 15rem;
    justify-content: space-between;
    align-items: center;
    gap: var(--s-8);
  }
  .hero--frame .store-badges img {
    height: 3rem;
  }
  .hero--frame .hero-copy {
    align-content: center;
  }
  .hero--frame .hero-cards {
    justify-self: end;
    align-self: center;
    width: 15rem;
    margin-top: 0;
    gap: var(--s-2);
  }
  .hero--frame .hero-card {
    padding: 1rem 1.125rem;
  }
  .hero--frame .hero-card-title {
    font-size: var(--t-lg);
  }
}
@media (min-width: 64rem) and (max-width: 90rem) {
  /* F10 1280-ish: the copy keeps the spec's 45 % column; the headline size is capped so the
     18ch measure fits it at ≤4 lines instead of the column growing to 55 %. */
  .hero--frame {
    --t-hero: clamp(2.25rem, 1.5rem + 3vw, 3.25rem);
  }
  .hero--frame .hero-media img {
    object-position: 48% 60%; /* keeps the car's bonnet clear of the card column */
  }
}

/* ---------- 9.2 Features: phone in a darker panel left, 2×4 glass cards right ---------- */
.split--panel {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: minmax(0, 1fr);
}
/* S3: a near-square darker panel (4:5) with the phone filling ~half its width and most
   of its height, the floor glow pooled under the phone's lower half. */
.phone-panel {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--n-900);
  border: 1px solid var(--border);
  border-radius: 32px;
  padding: 1.5rem;
  min-height: 22rem;
  display: grid;
  place-items: center;
}
.phone-panel::before {
  /* floor glow, contained by the panel and anchored to its bottom */
  content: "";
  position: absolute;
  left: 50%;
  bottom: -20%;
  width: 110%;
  height: 55%;
  transform: translateX(-50%);
  z-index: -1;
  background: var(--glow);
}
.phone-panel .phone {
  width: min(14rem, 70%);
  box-shadow: 0 40px 80px -24px hsl(0 0% 0% / 0.7);
}
.panel-copy {
  min-inline-size: 0;
}
.glass-grid {
  list-style: none;
  display: grid;
  gap: var(--s-3);
  grid-template-columns: minmax(0, 1fr);
}
.icon-card {
  padding: clamp(1.5rem, 3.5vw, var(--s-5));
  border-radius: 24px;
  min-inline-size: 0;
}
.icon-card svg {
  width: 3rem;
  height: 3rem;
  color: var(--accent-text);
  margin-bottom: var(--s-3);
}
.icon-card h3 {
  font-size: var(--t-xl);
  color: var(--fg);
  margin-bottom: 0.35rem;
  max-width: 14ch;
}
.icon-card p {
  color: var(--fg-soft);
  font-size: var(--t-sm);
}
@media (min-width: 40rem) {
  .phone-panel .phone {
    width: min(17.5rem, 100%);
  }
  .glass-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .icon-card svg {
    width: 2.75rem;
    height: 2.75rem;
  }
}
/* F6: the light app screenshot must not be the brightest thing in a dark panel — dim it a
   touch and let the floor glow carry the light. */
.phone-panel .phone img {
  filter: brightness(0.9);
}
@media (min-width: 64rem) {
  /* F6: S3 layout — the panel takes the left third for the heading row plus two card rows;
     cards 1–4 sit beside it as a 2×2, cards 5–8 run as a full-width row of four underneath.
     .panel-copy and the list are flattened into the section grid (display: contents keeps
     the list semantics); the panel no longer needs its own 4:5 box or to stick. */
  .split--panel {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--s-4);
    align-items: stretch;
  }
  .phone-panel {
    grid-column: 1 / span 4;
    grid-row: 1 / span 3;
    min-height: 0;
  }
  .phone-panel .phone {
    width: min(62%, 20rem);
  }
  .panel-copy,
  .glass-grid {
    display: contents;
  }
  .panel-copy .section-head {
    grid-column: 5 / -1;
    grid-row: 1;
    margin-bottom: 0;
    align-self: end;
  }
  .glass-grid .icon-card:nth-child(-n + 4) {
    grid-column: span 4;
  }
  .glass-grid .icon-card:nth-child(n + 5) {
    grid-column: span 3;
  }
  .icon-card {
    padding: var(--s-5);
  }
}

/* ---------- 9.2b Features: one photo cut into the tiles (2026-09-02) ----------
   One dark atmosphere photo (own white Mach-E in the shared parking bay, alt-less
   decoration) lies behind the whole section grid; the phone panel and the eight tiles are
   windows into it, like the hero cards over the hero photo. site.js measures the grid and
   sets background-size/position per window (inline CSSOM longhands, which the CSP allows —
   it blocks style *attributes* in markup, not element.style) and re-runs on resize. Without
   JS every window shows its own `cover` crop of the same photo, which still reads as one
   material. The tint is heavier at the top-left, where the text sits; body text
   (--fg-soft, 0.9375rem) must stay ≥ 4.5:1 over the brightest slice — measured 2026-09-02
   with the bay photo, see TEMPLATE-NOTES. */
.has-cutout {
  --cutout-img: url("/assets/img/features/bay-1280.webp");
  --cutout-tint-a: hsl(20 12% 6% / 0.7);
  --cutout-tint-b: hsl(20 12% 6% / 0.44);
}
@media (min-resolution: 1.5dppx) {
  .has-cutout {
    --cutout-img: url("/assets/img/features/bay-2048.webp");
  }
}
/* the runway ground is the features clip's first frame (16:9, the street where the hero
   ended), so the poster→clip swap is invisible and the :has()-less/never-ready fallback
   slices come from the same picture; the stacked layouts keep the square bay photo.
   Geometry: data-cutout-ratio-runway on the canvas. */
.section--features.is-runway .has-cutout {
  --cutout-img: url("/assets/img/features/street-d-1280.webp");
}
@media (min-resolution: 1.5dppx) {
  .section--features.is-runway .has-cutout {
    --cutout-img: url("/assets/img/features/street-d-2048.webp");
  }
}
.has-cutout .icon-card {
  background:
    linear-gradient(160deg, var(--cutout-tint-a), var(--cutout-tint-b)) padding-box,
    var(--cutout-img) center / cover no-repeat padding-box,
    var(--glass-hairline) border-box;
  background-color: var(--n-900);
}
.has-cutout .phone-panel {
  background:
    linear-gradient(160deg, var(--cutout-tint-b), var(--cutout-tint-a)) padding-box,
    var(--cutout-img) center / cover no-repeat padding-box;
  background-color: var(--n-900);
}

/* ---------- 9.2c Features: one photo that breaks into the tiles (2026-09-02) ----------
   Poul: the tiles should start as one assembled picture and, driven by the scrollbar while
   the section stays locked, break apart into the tiles; each tile ends with a "Læs mere"
   link that appears last. Mechanics: the same bay photo is painted once more on the grid
   container itself (::before, aligned by site.js with the identical cover geometry), the
   tiles carry no tint, hairline, shadow or content at progress 0 — so the grid reads as one
   seamless photo with rounded gaps that show the same pixels. As --p rises the container
   photo fades (the gaps go dark), each tile's tint/hairline/shadow scale in with its own
   --u (a stagger via --a: phone first, tiles 1–4, tiles 5–8), the content rises in with the
   tile, and the links come last (--v). Tiles are never translated — moving a window would
   break the seam — only their content moves. At --u = 1 the tint is exactly the 9.2b pair
   (.7/.44), so the measured body-text contrast is unchanged.
   Three modes, decided by site.js:
   • runway (`.is-runway`, ≥ 64rem and a viewport tall enough to hold the whole grid): the
     section becomes a sticky runway (100svh + --fx-runway of scroll), the grid is fitted to
     the viewport (auto + three 1fr rows, compact tiles, height-sized phone) and --p runs
     0..1 along the runway; the scrub clip in the phone follows the same --p.
   • observer (JS, but the grid does not fit or the viewport is narrow): each tile and the
     phone panel reveal on their own as they enter (`.is-in`, registered-property transitions),
     the container photo fades once the first one is in (`.is-open`).
   • none (no JS, reduced motion): everything at its final state.
   Registered properties so the observer mode can *transition* a number that several
   declarations read (tint alpha, hairline alpha, content opacity, shadow). */
@property --u { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --w { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --v { syntax: "<number>"; inherits: true; initial-value: 1; }
.section--features {
  --p: 1;
}
.section--features .has-cutout {
  position: relative;
  isolation: isolate;
}
.section--features .has-cutout::before {
  /* the whole photo behind the grid; size/position come from site.js (same cover geometry
     as the windows), `cover` until then */
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: 32px;
  background: var(--cutout-img) no-repeat;
  background-size: var(--cut-size, cover);
  background-position: var(--cut-pos, center);
  opacity: 0;
  pointer-events: none;
}
.section--features .has-cutout .icon-card {
  /* the photo layer is clipped to the border box (not padding-box as in 9.2b): the 1px
     border ring would otherwise show the n-900 background-colour as a dark outline while
     the hairline is still at alpha 0 — and the slice offset site.js computes is then exact */
  --u: 1;
  --v: 1;
  background:
    linear-gradient(160deg, hsl(20 12% 6% / calc(0.7 * var(--u))), hsl(20 12% 6% / calc(0.44 * var(--u)))) padding-box,
    var(--cutout-img) center / cover no-repeat border-box,
    linear-gradient(155deg, hsl(0 0% 100% / calc(0.3 * var(--u))), hsl(0 0% 100% / calc(0.06 * var(--u))) 40%, hsl(22 82% 60% / calc(0.1 * var(--u)))) border-box;
  background-color: var(--n-900);
  box-shadow:
    inset 0 1px 0 hsl(0 0% 100% / calc(0.1 * var(--u))),
    0 24px 48px -16px hsl(0 0% 0% / calc(0.45 * var(--u)));
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.section--features .icon-card > svg,
.section--features .icon-card > h3,
.section--features .icon-card > p {
  opacity: var(--u);
  transform: translateY(calc((1 - var(--u)) * 1rem));
}
.section--features .icon-card > p {
  margin-bottom: 0;
}
.tile-more {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: auto;
  padding-top: 0.9rem;
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--accent-text);
  text-decoration: none;
  opacity: var(--v);
  transform: translateY(calc((1 - var(--v)) * 0.5rem));
}
.tile-more::after {
  content: "→";
  transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.tile-more:hover::after,
.tile-more:focus-visible::after {
  transform: translateX(0.2rem);
}
.tile-more:hover {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.tile-more:focus-visible {
  /* an opacity-revealed link is still tabbable — never let focus land on nothing */
  opacity: 1;
  transform: none;
}
.section--features .has-cutout .phone-panel {
  --w: 1;
  background:
    linear-gradient(160deg, hsl(20 12% 6% / calc(0.44 * var(--w))), hsl(20 12% 6% / calc(0.7 * var(--w)))) padding-box,
    var(--cutout-img) center / cover no-repeat border-box;
  background-color: var(--n-900);
  border-color: hsl(0 0% 100% / calc(0.12 * var(--w)));
}
.section--features .phone-panel::before {
  opacity: var(--w);
}
html.sp-js .section--features .phone-panel .phone {
  opacity: var(--w);
}
/* runway: the timeline (fractions of the runway) */
.section--features.is-runway .has-cutout::before {
  opacity: calc(1 - clamp(0, var(--p) / 0.3, 1));
}
.section--features.is-runway .phone-panel {
  --w: clamp(0, (var(--p) - 0.04) / 0.28, 1);
}
/* §9.2c video ground (Poul 2026-09-02: "videoen bruges som baggrund til billedet og tiles i
   stedet for billedet, så videoen kører mens den revealer tiles"): in runway mode a clip is
   scrubbed in the frame over the whole runway and the windows are glass over it — no photo
   slice, no opaque ground — so the tiles break out while the car is still moving. The photo
   (::before) is the clip's poster and the fallback ground (observer mode, no JS, reduced
   motion, :has() unsupported, clip never decodable). EXPERIMENT: the hero clip stands in
   until the real one (the car driving out of the bay) is rendered. */
.section--features .cutout-video {
  display: none;
}
.section--features.is-runway .cutout-video {
  /* absolute: no grid cell; painted above the photo (same z-index, later in order) and
     under the windows */
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 62% 50%;
  border-radius: var(--radius-frame);
  z-index: -1;
  opacity: 0;
  pointer-events: none;
}
/* the clip shows only where the glass rules below can apply — without :has() the windows
   would keep their static photo slices over a moving street */
@supports selector(.a:has(> .b)) {
  .section--features.is-runway .cutout-video.is-ready {
    /* same poster-first rule as the hero: the still is sharper than the upscaled clip */
    opacity: clamp(0, var(--p, 0) * 40, 1);
  }
}
/* the end state matches the photo pass (Poul 2026-09-02: "should the background video
   outside the tiles not be gone when we are at the end of the scroll like with the image
   was?"): the gaps go dark over the last stretch while the windows keep the clip. --g is
   the gap alpha; the clip's mask is the union of a full layer at --g and the window
   rectangles (site.js sets --cut-mask, an SVG of rounded rects in the same geometry as
   the slices), and the poster fades with it. */
.section--features.is-runway .has-cutout {
  --g: calc(1 - clamp(0, (var(--p) - 0.66) / 0.24, 1));
}
.section--features.is-runway .has-cutout:has(> .cutout-video.is-ready)::before {
  /* the poster leaves as the clip fades in (same p × 40 ramp), NOT with the gaps: while
     the gap layer fades the clip is half-transparent there, and a poster fading at the
     same rate showed through it — the parked car came back behind the empty bay
     (Poul 2026-09-02: "bilen er der to gange") */
  opacity: calc(1 - clamp(0, var(--p) * 40, 1));
}
.section--features.is-runway .cutout-video {
  mask-image: linear-gradient(hsl(0 0% 0% / var(--g)), hsl(0 0% 0% / var(--g))), var(--cut-mask, linear-gradient(transparent, transparent));
  mask-size: 100% 100%;
  mask-repeat: no-repeat;
  mask-composite: add;
}
.section--features.is-runway .has-cutout:has(> .cutout-video.is-ready) .icon-card {
  /* the same tint and hairline as the photo windows (alpha × --u), minus the slice and the
     n-900 ground: at --u:0 the tile is the clip, at --u:1 the text sits on the .7/.44 tint */
  background:
    linear-gradient(160deg, hsl(20 12% 6% / calc(0.7 * var(--u))), hsl(20 12% 6% / calc(0.44 * var(--u)))) padding-box,
    linear-gradient(155deg, hsl(0 0% 100% / calc(0.3 * var(--u))), hsl(0 0% 100% / calc(0.06 * var(--u))) 40%, hsl(22 82% 60% / calc(0.1 * var(--u)))) border-box;
  background-color: transparent;
}
.section--features.is-runway .has-cutout:has(> .cutout-video.is-ready) .phone-panel {
  background: linear-gradient(160deg, hsl(20 12% 6% / calc(0.44 * var(--w))), hsl(20 12% 6% / calc(0.7 * var(--w)))) padding-box;
  background-color: transparent;
}
/* the heading is revealed too (Poul 2026-09-02: on the photo from the start it "ser lidt dumt
   ud") — it rises in between the phone and the first tile */
.section--features .section-head h2 {
  --u: 1;
  opacity: var(--u);
  transform: translateY(calc((1 - var(--u)) * 1rem));
}
.section--features.is-runway .section-head h2 {
  --u: clamp(0, (var(--p) - 0.06) / 0.14, 1);
}
html.sp-js .section--features.is-runway .phone-panel .phone {
  transform: translateY(calc((1 - var(--w)) * 12%)) rotate(calc((var(--w) - 1) * 4deg));
}
.section--features.is-runway .icon-card {
  /* the stagger width (0.22) is also the constant in site.js cardReveals */
  --u: clamp(0, (var(--p) - var(--a, 0.2)) / 0.22, 1);
  --v: clamp(0, (var(--p) - 0.84) / 0.14, 1);
}
.section--features.is-runway .tile-more {
  --b: 0.84; /* read by site.js offGuard */
}
.section--features.is-runway [data-off] {
  visibility: hidden;
}
.section--features.is-runway .icon-card:nth-child(1) { --a: 0.14; }
.section--features.is-runway .icon-card:nth-child(2) { --a: 0.2; }
.section--features.is-runway .icon-card:nth-child(3) { --a: 0.26; }
.section--features.is-runway .icon-card:nth-child(4) { --a: 0.32; }
.section--features.is-runway .icon-card:nth-child(5) { --a: 0.42; }
.section--features.is-runway .icon-card:nth-child(6) { --a: 0.48; }
.section--features.is-runway .icon-card:nth-child(7) { --a: 0.54; }
.section--features.is-runway .icon-card:nth-child(8) { --a: 0.6; }
/* observer: each window reveals on its own as it enters */
html.sp-js .section--features:not(.is-runway) .has-cutout::before {
  opacity: 1;
  transition: opacity 1.4s ease-out;
}
html.sp-js .section--features:not(.is-runway).is-open .has-cutout::before {
  opacity: 0;
}
html.sp-js .section--features:not(.is-runway) .icon-card {
  transition: --u 0.9s cubic-bezier(0.22, 0.61, 0.36, 1), --v 0.7s ease-out 0.55s;
}
html.sp-js .section--features:not(.is-runway) .icon-card:not(.is-in) {
  --u: 0;
  --v: 0;
}
html.sp-js .section--features:not(.is-runway) .phone-panel {
  transition: --w 0.9s ease-out;
}
html.sp-js .section--features:not(.is-runway) .phone-panel:not(.is-in) {
  --w: 0;
}
html.sp-js .section--features:not(.is-runway) .section-head h2 {
  transition: --u 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
}
html.sp-js .section--features:not(.is-runway):not(.is-open) .section-head h2 {
  --u: 0;
}
/* runway layout: the grid fitted to one viewport so every tile, including the bottom row,
   is on screen while the section is locked */
@media (min-width: 64rem) {
  .section--features.is-runway {
    /* the runway frame is the hero frame's twin: the same --frame-inset from the viewport on
       all sides (so the gap under the hero card equals its side inset), the same radius and
       hairline while the photo shows. Poul 2026-09-02: a 72rem photo under a full-bleed hero
       card "virker underlig". The grid inside keeps the ordinary gutter. */
    --fx-pad: var(--frame-inset);
    --fx-runway: 150vh;
    min-height: calc(100svh + var(--fx-runway));
    padding-block: var(--fx-pad);
  }
  .section--features.is-runway > .has-cutout {
    position: sticky;
    top: var(--fx-pad);
    height: min(calc(100svh - 2 * var(--fx-pad)), 58rem);
    width: auto;
    max-width: none;
    margin-inline: var(--frame-inset);
    /* the photo is frame-wide, the grid is not: it keeps the ordinary 72rem container
       measure centred in the frame and the windows use the part of the photo under them
       (Poul 2026-09-02: "de kan bare bruge den del af billedet") */
    padding-inline: max(var(--gutter), calc((100% - var(--container)) / 2 + var(--gutter)));
    gap: var(--s-3);
    grid-template-rows: auto minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
  }
  .section--features.is-runway .has-cutout::before {
    border-radius: var(--radius-frame);
    box-shadow: inset 0 0 0 1px hsl(0 0% 100% / 0.1);
  }
  .section--features.is-runway .section-head h2 {
    /* sits straight on the photo until the tiles come; the hero copy does the same */
    text-shadow: 0 1px 2px hsl(0 0% 0% / 0.4), 0 8px 32px hsl(0 0% 0% / 0.35);
  }
  .section--features.is-runway .icon-card {
    padding: 1.25rem 1.375rem 1.125rem;
    overflow: hidden;
  }
  .section--features.is-runway .icon-card svg {
    width: 2.25rem;
    height: 2.25rem;
    margin-bottom: 0.6rem;
  }
  .section--features.is-runway .icon-card h3 {
    margin-bottom: 0.25rem;
  }
  .section--features.is-runway .tile-more {
    padding-top: 0.6rem;
  }
  .section--features.is-runway .phone-panel {
    /* the panel's one implicit row would size itself to the phone's intrinsic height (its
       stretched width × 19.5/9 ≈ 690px) and centre the height-capped phone in that, pushing it
       out of the panel's bottom; one 1fr row is the panel's own height */
    grid-template-rows: minmax(0, 1fr);
    min-height: 0;
  }
  .section--features.is-runway .phone-panel .phone {
    width: auto;
    height: min(84%, 30rem);
  }
}

/* ---------- 9.3 How it works: four rounded tiles, screen crop at the foot of 3–4 ---------- */
.steps--tiles {
  display: grid;
  gap: var(--s-3);
  grid-template-columns: minmax(0, 1fr);
}
.tile {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--n-900);
  border: 1px solid var(--border);
  border-radius: 28px;
  padding: clamp(1.25rem, 3vw, 1.75rem);
  display: grid;
  align-content: start;
  min-inline-size: 0;
}
/* The 01–04 counter stays in flow above the icon/title (the .step default), so a title
   never has to make room for it. */
.tile h3 {
  font-size: clamp(1.1rem, 1.4vw, 1.35rem);
  text-wrap: balance;
}
/* F7: all four tiles share one anatomy — counter, title, text at the top, a visual at the
   foot. In tiles 1–2 the visual is the icon plate, moved after the text (order) and pinned
   to the tile's bottom edge on a floor glow; in 3–4 it is the screen bezel. */
.tile-icon {
  width: 5rem;
  height: 5rem;
  padding: 1.25rem;
  border-radius: 50%;
  background: hsl(20 10% 16%);
  border: 1px solid var(--border);
  color: var(--accent-text);
  order: 4;
  align-self: end;
  justify-self: center;
  margin-top: var(--s-6);
  margin-bottom: 0.5rem;
  box-shadow: 0 8px 24px -8px hsl(0 0% 0% / 0.5);
}
.tile:not(.tile--screen)::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -35%;
  width: 90%;
  height: 55%;
  transform: translateX(-50%);
  z-index: -1;
  background: var(--glow);
}
.tile--screen {
  padding-bottom: 12rem; /* room for the taller bezel (list crop ≈ 10rem) plus air above it */
}
/* The screen crop sits in a dark bezel (n-800, 6px lip, 28px top corners, a camera pill in
   its forehead) that runs out through the tile's bottom edge. The crops render at their
   natural aspect on a near-black screen ground, so the bezel height follows the crop
   (calendar shorter than list) instead of padding the shortfall with orange. */
.tile-bezel {
  position: absolute;
  left: 50%;
  bottom: -2px;
  transform: translateX(-50%);
  width: min(86%, 16rem);
  padding: 1.1rem 6px 0;
  background: var(--n-800);
  border: 1px solid hsl(0 0% 100% / 0.12);
  border-bottom: 0;
  border-radius: 28px 28px 0 0;
  box-shadow: 0 -16px 40px hsl(0 0% 0% / 0.45);
  overflow: hidden;
}
.tile-bezel::before {
  content: "";
  position: absolute;
  top: 0.4rem;
  left: 50%;
  transform: translateX(-50%);
  width: 32%;
  height: 6px;
  border-radius: 999px;
  background: var(--n-950);
}
.tile-shot {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 18px 18px 0 0;
  background: var(--n-950);
}
@media (min-width: 40rem) {
  .steps--tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* every tile the same height, across rows too (768: 2×2) */
    grid-auto-rows: 1fr;
    align-items: stretch;
  }
  .tile {
    grid-template-rows: auto auto auto minmax(0, 1fr);
  }
  .tile--screen {
    padding-bottom: 12.5rem;
  }
}
@media (min-width: 64rem) {
  .steps--tiles {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--s-4);
  }
}

/* ---------- 9.4 Who: the tint band a step lighter on the home page ---------- */
.page-home .section--tint {
  background: hsl(20 12% 9%);
  padding-block: var(--s-10);
}

/* ---------- 9.5 Trust: icon list left, tilted phone exiting bottom-right ---------- */
/* .section--stage clips the phone (no horizontal scroll) and has no bottom padding, so
   the phone is cut by the section's bottom edge — it exits through the frame as in S1.
   Also used by the closing band. */
.section--stage {
  position: relative;
  overflow: hidden;
  padding-bottom: 0;
}
.split--stage {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: minmax(0, 1fr);
}
.trust-list {
  display: grid;
  gap: var(--s-4);
}
.trust-item--plate {
  gap: var(--s-3);
}
.trust-item--plate svg {
  width: 4.5rem;
  height: 4.5rem;
  padding: 1.15rem;
  border-radius: 18px;
  background: linear-gradient(160deg, hsl(22 82% 46% / 0.25), transparent), var(--n-800);
  border: 1px solid var(--border);
  margin-top: 0;
}
.phone-stage {
  position: relative;
  isolation: isolate;
  height: 26rem; /* fixed: the in-flow phone overflows it and the section clips */
}
.phone-stage::before {
  /* soft floor glow, wider than the stage, core under the phone's lower half */
  content: "";
  position: absolute;
  inset: -10% -20% -30% -10%;
  z-index: -1;
  background: radial-gradient(
    ellipse at 45% 55%,
    hsl(22 82% 46% / 0.55),
    hsl(22 82% 46% / 0.18) 40%,
    transparent 72%
  );
  filter: blur(40px);
}
.phone-stage--tilt .phone-3d {
  width: min(20rem, 80%);
  margin-inline: auto;
  transform: rotate(var(--tilt-base, -8deg)) translate(10%, 8%);
  transform-origin: 50% 100%;
}
@media (min-width: 64rem) {
  .split--stage {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    align-items: start;
  }
  .split--stage > :first-child {
    padding-bottom: var(--section-y);
  }
  .phone-stage {
    height: auto;
    min-height: 34rem;
  }
  .phone-stage--tilt .phone-3d {
    /* the dominant element of the row (S1): ~30 % of the viewport, bottom leaving the
       section through its lower-right corner */
    /* F1: anchored to the stage's left edge and rotated about its top-left corner, so the
       phone grows to the right (into the stage) at wide viewports instead of leaning left
       over the trust-list plates. */
    position: absolute;
    left: 0;
    right: auto;
    top: var(--s-6);
    width: clamp(21rem, 30vw, 30rem);
    transform-origin: 0 0;
    transform: rotate(-9deg);
  }
}

/* ---------- 9.5f Trust runway: the page locks and the scroll scrubs the booking clip ----------
   Poul 2026-09-02: "brug scroll til at styre den video / siden låses fast før den spiller app
   videoen så vi viser folk hele booking oplevelsen" — the same runway machinery as the features
   section (9.2c). The section grows to one viewport plus --fx-runway of scroll, the container
   sticks, and the section's --p scrubs the clip inside the phone (p .06–.9, the last tenth holds
   the final frame like the hero) while the four plates reveal one after the other.

   overflow: clip, NOT hidden: hidden makes the section a scroll container and sticky then has
   nowhere to travel. The container gets an explicit height (not min-height) because the stage's
   container-type: size needs a resolved row to size the phone from.

   Outside the runway (narrow, short, no JS, reduced motion) nothing here applies: the section
   keeps its 9.5d/9.5e layout, the clip loops, and the plates keep their resting pose. */
@media (min-width: 64rem) {
  .section--trust.is-runway {
    display: block;
    overflow: clip;
    padding-block: 0;
    --fx-runway: 150vh;
    --fx-stage: min(100svh, 62rem);
    min-height: calc(100svh + var(--fx-runway));
  }
  .section--trust.is-runway > .container {
    position: sticky;
    top: max(0px, calc((100svh - var(--fx-stage)) / 2));
    height: var(--fx-stage);
    padding-block: var(--section-y) 0;
  }
  /* the copy column no longer needs the bottom pad that balanced the static row */
  .section--trust.is-runway .split--stage > :first-child {
    padding-bottom: 0;
  }
  /* the plates arrive one by one; --a is also read by site.js (cardReveals) */
  .section--trust.is-runway .trust-item--plate {
    --u: clamp(0, (var(--p) - var(--a, 0.2)) / 0.22, 1);
    opacity: var(--u);
    transform: translateY(calc((1 - var(--u)) * 1.25rem));
    will-change: opacity, transform;
  }
  .section--trust.is-runway .trust-item--plate:nth-child(1) { --a: 0.08; }
  .section--trust.is-runway .trust-item--plate:nth-child(2) { --a: 0.22; }
  .section--trust.is-runway .trust-item--plate:nth-child(3) { --a: 0.36; }
  .section--trust.is-runway .trust-item--plate:nth-child(4) { --a: 0.5; }
  /* the store badges do NOT fade in with the runway: Apple's badge guidelines forbid
     animating the badge, so it is on screen from the first frame of the stage. */
  /* the pose drift now spans 150vh instead of one crossing: dampen it so the phone does not
     creep across the whole lock */
  html.sp-js .section--trust.is-runway .phone-stage--tilt .phone-3d {
    transform: rotate(calc(var(--tilt-base, 8deg) + (0.5 - var(--p, 0.5)) * 2.5deg)) translateY(calc((0.5 - var(--p, 0.5)) * 3%));
  }
}

/* ---------- 9.6 Pricing teaser: a tint band with the figure at display size (F8) ---------- */
.section--pricing {
  background: var(--surface-1);
  border-block: 1px solid var(--border);
}
.price-figure {
  display: block;
  font-size: var(--t-3xl);
  font-weight: 600;
  color: var(--fg);
  letter-spacing: -0.02em;
  line-height: 1.12;
  margin-bottom: var(--s-2);
}
.section--pricing .price-card p {
  color: var(--fg-soft);
}

/* ---------- 9.7 Closing: two phones on a floor glow under the centred head + badges ---------- */
/* The band is itself a rounded card inset like the hero (top corners only; the footer
   hairline closes it), so the page ends the way it began instead of with a flat seam. */
.page-home .section--closing {
  margin-inline: var(--frame-inset);
  border: 1px solid var(--border);
  border-bottom: 0;
  border-radius: var(--radius-frame) var(--radius-frame) 0 0;
}
.section--closing.section--stage {
  padding-bottom: 0; /* the phones exit through the band's bottom edge */
}
.phone-fan {
  position: relative;
  isolation: isolate;
  height: clamp(18rem, 42vw, 30rem);
  margin-top: var(--s-8);
  overflow: hidden; /* a clean cut at the band edge, no fog */
}
.section--closing .store-badges {
  gap: var(--s-4);
  margin-top: var(--s-6);
}
@media (min-width: 24rem) and (max-width: 63.99rem) {
  /* like the hero: one row of two 40px plates on phones */
  .page-home .section--closing .store-badges {
    flex-wrap: nowrap;
  }
  .page-home .section--closing .store-badges img {
    height: 2.5rem;
  }
}
.phone-fan::before {
  /* the orange pool the phones stand in (S2) */
  content: "";
  position: absolute;
  left: 50%;
  bottom: -20%;
  width: 110%;
  height: 80%;
  transform: translateX(-50%);
  z-index: -1;
  background:
    radial-gradient(ellipse at 50% 100%, hsl(22 82% 56% / 0.55), transparent 45%),
    var(--glow);
}
.phone-fan .phone {
  position: absolute;
  top: 0;
  left: 50%;
  width: min(19rem, 62%);
}
.phone--front {
  transform: translateX(-50%);
  z-index: 2;
}
.phone--back {
  transform: translateX(-74%) rotate(-7deg) scale(0.92);
  transform-origin: 50% 100%;
  opacity: 0.9;
  filter: brightness(0.85);
  z-index: 1;
}
@media (max-width: 30rem) {
  /* 320/390: one phone; a fanned second phone would be clipped by the fan's left edge
     (its -7deg tilt pushes the top corner ~12 % of its height further out) */
  .phone--back {
    display: none;
  }
}
@media (min-width: 30rem) and (max-width: 63.99rem) {
  /* two phones: narrow enough that the tilted back phone stays inside the fan */
  .phone-fan .phone {
    width: min(19rem, 52%);
  }
  .phone--front {
    transform: translateX(-10%);
  }
}
@media (min-width: 64rem) {
  .phone-fan .phone {
    width: 20rem;
  }
  /* the pair's bounding box (tilt included) is centred under the headline */
  .phone--front {
    transform: translateX(-2%);
  }
  .phone--back {
    transform: translateX(-86%) rotate(-8deg) scale(0.94);
  }
}
/* Home footer: more air after the fan */
.page-home .site-footer {
  padding-top: var(--s-10);
}


/* ---------- 9.10 Subpages: the same framed cards without the home-only layouts ---------- */
/* Charging hero: the photo inside the rounded frame like the home page, but a title hero —
   one column, no link cards, shorter than the fold. Declared after §9.1 so the
   .page-charging rules win on paint order as well as specificity. */
:is(.page-charging, .page-press) .hero--frame {
  min-height: 0;
  padding: var(--frame-inset) var(--frame-inset) 0;
}
:is(.page-charging, .page-press) .hero-frame {
  /* the copy is in a .container, so the frame adds no inline padding of its own and the
     h1 lines up with the wordmark in the header's (identical) container. 16rem of photo
     above the copy on phones: the scrim reaches .78 at 16rem. */
  padding: 16rem 0 var(--s-8);
  align-items: end;
}
:is(.page-charging, .page-press) .hero--frame .hero-grid {
  grid-template-columns: minmax(0, 1fr);
}
:is(.page-charging, .page-press) .hero--frame .hero-copy {
  align-content: end;
}
:is(.page-charging, .page-press) .hero--frame .hero-media img {
  object-position: 55% 45%;
  filter: none;
}
@media (min-width: 64rem) {
  :is(.page-charging, .page-press) .hero--frame {
    min-height: 0;
    padding: var(--frame-inset) var(--frame-inset) 0;
  }
  :is(.page-charging, .page-press) .hero-frame {
    min-height: min(62svh, 36rem);
    align-items: end;
    padding: calc(var(--header-h) + var(--s-8)) 0 var(--frame-pad);
  }
  :is(.page-charging, .page-press) .hero--frame .hero-grid {
    grid-template-columns: minmax(0, 1fr);
    align-items: end;
  }
  :is(.page-charging, .page-press) .hero--frame .hero-copy {
    align-content: end;
  }
  .page-charging .hero--frame .hero-media img {
    /* 1280: the scaled photo is ~750px tall in a ~576px card; 88 % shows the whole car,
       roof to wheels (same framing as before the card). */
    object-position: 65% 88%;
  }
}
/* F12: the one-sentence "help setting up a charger" split shares the closing band with the
   contact head instead of having a section of its own; a hairline separates the two. */
.page-charging .section--closing .split--setup {
  padding-bottom: var(--s-6);
  margin-bottom: var(--s-8);
  border-bottom: 1px solid var(--border);
}
.page-charging .section--closing .split--setup .lead {
  color: var(--fg-soft);
}
@media (min-width: 90rem) {
  .page-charging .hero--frame .hero-media img {
    object-position: 65% 62%;
  }
}
/* The rounded card meets the next band: a little dark ground between them, and the band's
   own top hairline goes (the card edge is the line). */
:is(.page-charging, .page-press) .hero--frame + .section--tint {
  border-top: 0;
  margin-top: var(--frame-inset);
}

/* Glass cards (pricing, support, 404): the S3 card recipe at rest — gradient hairline on the
   border-box behind a translucent fill, an inner top highlight, no blur (nothing moves
   behind them). Two classes so it beats `.section--tint .card` and `.card--tint`. */
.card.card--glass {
  border: 1px solid transparent;
  border-radius: var(--radius-xl);
  background:
    linear-gradient(hsl(20 10% 12% / 0.85), hsl(20 10% 9% / 0.85)) padding-box,
    var(--glass-hairline) border-box;
  box-shadow:
    inset 0 1px 0 hsl(0 0% 100% / 0.1),
    0 24px 48px -16px hsl(0 0% 0% / 0.45);
}
/* The accent card keeps its orange, as a lit top edge and a faint glow in the fill instead
   of the 3px bar (the 1px transparent border above would otherwise swallow the bar). */
.card.card--accent.card--glass {
  background:
    radial-gradient(40rem 12rem at 20% 0%, hsl(22 82% 46% / 0.22), transparent 70%)
      padding-box,
    linear-gradient(hsl(20 10% 12% / 0.85), hsl(20 10% 9% / 0.85)) padding-box,
    linear-gradient(
        180deg,
        hsl(22 90% 58% / 0.75),
        hsl(0 0% 100% / 0.14) 35%,
        hsl(22 82% 60% / 0.12)
      )
      border-box;
  box-shadow:
    inset 0 1px 0 hsl(22 90% 62% / 0.55),
    0 24px 48px -16px hsl(0 0% 0% / 0.45);
}

/* FAQ as a rounded glass panel: the rows keep their hairlines inside it, the panel takes
   over the outer edges. */
.faq.faq--glass {
  border-radius: var(--radius-xl);
  border: 1px solid transparent;
  background:
    linear-gradient(hsl(20 10% 12% / 0.85), hsl(20 10% 9% / 0.85)) padding-box,
    var(--glass-hairline) border-box;
  box-shadow:
    inset 0 1px 0 hsl(0 0% 100% / 0.1),
    0 24px 48px -16px hsl(0 0% 0% / 0.45);
  padding-inline: clamp(1rem, 3vw, 1.75rem);
  /* the +/- toggle reach still caps the row (see .faq), the panel just gets its padding */
  max-width: calc(38rem + 2 * clamp(1rem, 3vw, 1.75rem));
}
.faq.faq--glass details:last-child {
  border-bottom: 0;
}
.faq.faq--glass summary {
  padding-block: 1.125rem;
}

/* ---------- Motion ---------- */
/* The only transitions on the site: hero-card border/lift, button colours, badge
   opacity, hamburger bars. Nothing is hidden before paint. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  .hero-card,
  .hero-card-chevron,
  .btn,
  .store-badges--paper a,
  .nav-toggle .bars::before,
  .nav-toggle .bars::after {
    transition: none;
  }
  .hero-card:hover,
  .hero-card:focus-visible,
  .hero-card:hover .hero-card-chevron,
  .hero-card:focus-visible .hero-card-chevron {
    transform: none;
  }
}


/* ---------- Icon set (2026-09-02) ----------
   Own 24-grid line icons, 1.75 stroke, round caps. Source: assets/img/icons/<name>.svg,
   inlined by tools/inline-icons.py. Every icon carries exactly one orange element
   (`.ic-dot`, the dot from the logo) — the booked day, the alert badge, the neighbour's
   head, the connected marker — while the strokes stay in --fg. */
svg[data-icon] {
  color: var(--fg);
}
svg[data-icon] .ic-dot {
  fill: var(--brand-500);
  stroke: none;
}

/* ---------- Icon motion (2026-09-02) ----------
   Two layers, both off under prefers-reduced-motion and without JS.
   1) Reveal: site.js adds `ic-js` to <html>, sets each stroke's dasharray/offset to its
      own length (CSSOM), and flips `is-in` on the icon when it scrolls into view — the
      strokes draw themselves (transition below), then the orange dot pops.
   2) Hover: every icon has one small motion on its own part (`.ic-*` hooks in the source
      SVGs) when its card is hovered. Keyframes, so they replay on every hover. */
html.ic-js svg[data-icon] :is(path, circle, rect):not(.ic-dot, [fill="currentColor"]) {
  transition: stroke-dashoffset 1.1s cubic-bezier(0.22, 0.61, 0.36, 1);
}
html.ic-js svg[data-icon] .ic-dot {
  transform: scale(0);
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.85s;
}
html.ic-js svg[data-icon].is-in .ic-dot {
  transform: scale(1);
}
/* hover hooks — parts rotate/scale about their own box unless a view-box origin is given */
svg[data-icon] :is(.ic-swing, .ic-shake, .ic-tilt, .ic-hop, .ic-slide, .ic-press, .ic-rise, .ic-bar, .ic-b1, .ic-b2, .ic-b3, .ic-step, .ic-pulse, .ic-lens) {
  transform-box: fill-box;
  transform-origin: center;
}
svg[data-icon] .ic-swing { transform-origin: 50% 0; }
svg[data-icon] .ic-bar { transform-origin: 50% 100%; }
svg[data-icon] .ic-hand { transform-box: view-box; transform-origin: 12px 12px; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-swing { animation: ic-swing 0.9s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-pulse { animation: ic-pulse 0.9s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-step { animation: ic-step 1.2s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-blink { animation: ic-blink 0.7s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-lens { animation: ic-lens 0.7s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-hand { animation: ic-hand 1.2s cubic-bezier(0.34, 1.3, 0.64, 1); }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-shake { animation: ic-shake 0.6s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-b1 { animation: ic-bounce 0.8s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-b2 { animation: ic-bounce 0.8s ease-in-out 0.12s; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-b3 { animation: ic-bounce 0.8s ease-in-out 0.24s; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-hop { animation: ic-hop 0.9s cubic-bezier(0.34, 1.56, 0.64, 1); }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-slide { animation: ic-slide 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-lamp { animation: ic-lamp 0.9s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-check { animation: ic-draw 0.8s ease-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-tilt { animation: ic-tilt 0.9s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-bar { animation: ic-grow 0.8s cubic-bezier(0.22, 0.61, 0.36, 1); }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-bar:nth-of-type(2) { animation-delay: 0.06s; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-bar:nth-of-type(3) { animation-delay: 0.12s; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-bar:nth-of-type(4) { animation-delay: 0.18s; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-rise { animation: ic-rise 0.9s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-press { animation: ic-press 0.6s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-stem { animation: ic-glow 0.6s ease-in-out; }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-flicker { animation: ic-flicker 0.7s steps(1, end); }
:is(.icon-card, .tile, .trust-item):hover > svg[data-icon] .ic-spark { animation: ic-spark 0.7s ease-out; }
@keyframes ic-swing { 0%, 100% { transform: rotate(0); } 20% { transform: rotate(14deg); } 45% { transform: rotate(-11deg); } 70% { transform: rotate(6deg); } 85% { transform: rotate(-3deg); } }
@keyframes ic-pulse { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.7); } }
@keyframes ic-step { 0%, 100% { transform: translateY(0); } 33% { transform: translateY(5px); } 66% { transform: translateY(10px); } }
@keyframes ic-blink { 0%, 100% { transform: scale(1); opacity: 1; } 30% { transform: scale(0.2); opacity: 0.4; } 60% { transform: scale(1.4); } }
@keyframes ic-lens { 0%, 100% { transform: scale(1); } 30% { transform: scale(0.82); } }
@keyframes ic-hand { to { transform: rotate(360deg); } }
@keyframes ic-shake { 0%, 100% { transform: rotate(0); } 25% { transform: rotate(-7deg); } 50% { transform: rotate(6deg); } 75% { transform: rotate(-3deg); } }
@keyframes ic-bounce { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(-2.5px); } }
@keyframes ic-hop { 0%, 100% { transform: translate(0, 0); } 45% { transform: translate(0, 10px); } }
@keyframes ic-slide { 0% { transform: translateX(5px) scale(0.6); } 100% { transform: translateX(0) scale(1); } }
@keyframes ic-lamp { 0%, 100% { fill: currentColor; } 30%, 60% { fill: var(--brand-300); } }
@keyframes ic-draw { 0% { stroke-dasharray: 12; stroke-dashoffset: 12; } 100% { stroke-dasharray: 12; stroke-dashoffset: 0; } }
@keyframes ic-tilt { 0%, 100% { transform: rotate(0); } 35% { transform: rotate(-6deg) translateY(-1px); } }
@keyframes ic-grow { 0% { transform: scaleY(0.15); } 100% { transform: scaleY(1); } }
@keyframes ic-rise { 0% { transform: translateY(6px) scale(0.4); } 100% { transform: translateY(0) scale(1); } }
@keyframes ic-press { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(2.5px) scale(0.85); } }
@keyframes ic-glow { 0%, 100% { stroke: currentColor; } 40% { stroke: var(--brand-400); } }
@keyframes ic-flicker { 0%, 100% { opacity: 1; } 15% { opacity: 0.35; } 30% { opacity: 1; } 45% { opacity: 0.5; } 60% { opacity: 1; } }
@keyframes ic-spark { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(2); opacity: 0.5; } 100% { transform: scale(1); opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  svg[data-icon] * { animation: none !important; transition: none !important; }
  html.ic-js svg[data-icon] .ic-dot { transform: none; }
}

/* ---------- Scroll-linked phones (2026-09-02) ----------
   site.js writes `--p` (0 = the stage's top edge at the bottom of the viewport, 1 = its
   bottom edge at the top) on every [data-scroll-progress] element, rAF-throttled, via
   element.style (CSSOM, CSP-safe). The phones read it: the features phone drifts and tips as
   the panel passes, the trust phone settles from a steeper tilt, the closing fan lifts and
   opens. Every formula equals today's static pose at --p = .5, which is also the fallback
   value — so no JS and prefers-reduced-motion both render the resting design. When Poul's
   screen recordings land, the same --p drives video.currentTime; nothing here changes. */
html.sp-js .phone-panel .phone {
  transform: translateY(calc((0.5 - var(--p, 0.5)) * 10%)) rotate(calc((var(--p, 0.5) - 0.5) * 5deg));
  will-change: transform;
}
html.sp-js .phone-stage--tilt .phone-3d {
  transform: rotate(calc(var(--tilt-base, -8deg) - (0.5 - var(--p, 0.5)) * 8deg)) translate(10%, calc(8% + (0.5 - var(--p, 0.5)) * 14%));
  will-change: transform;
}
html.sp-js .phone-fan .phone--front {
  transform: translateX(-50%) translateY(calc((0.5 - var(--p, 0.5)) * 14%));
  will-change: transform;
}
html.sp-js .phone-fan .phone--back {
  transform: translateX(calc(-74% - (var(--p, 0.5) - 0.5) * 30%)) translateY(calc((0.5 - var(--p, 0.5)) * 18%)) rotate(calc(-7deg - (var(--p, 0.5) - 0.5) * 8deg)) scale(0.92);
  will-change: transform;
}

/* ---------- Hero video (2026-09-02, review build) ----------
   Clip B (Seedance 2.5, shipped at 1280×720, all-intra) sits over the hero photo and is
   scrubbed by scroll: site.js maps the frame's exit progress to video.currentTime. The
   photo stays underneath as poster and as the no-JS / reduced-motion / phone / data-saver
   rendering; the video only fades in once its first frame is decoded (.is-ready) AND the
   scroll has begun (--p > 0): the poster is the same picture at 1792 px, the clip is 1280 px
   scaled up, so at rest the sharp still must win (Poul 2026-09-02: the resting hero looked
   "pixeleret/grumset"). The clip is fully in by p .025 — a few tens of px of scroll. */
.hero--frame .hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 70% 40%;
  filter: saturate(1.05);
  z-index: -2;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
.hero--frame .hero-video.is-ready {
  opacity: clamp(0, var(--p, 0) * 40, 1);
}
@media (max-width: 47.99rem) {
  .hero--frame .hero-video {
    display: none; /* the portrait band uses the photo; the clip is landscape */
  }
}

/* Phone clip: the booking flow (a real Android e2e recording of the app) inside the features
   phone, scrubbed by the panel's scroll progress. Same fade-in contract as the hero clip; the
   poster <img> underneath is the clip's first frame. */
.phone .phone-video {
  position: absolute;
  inset: 0.7rem;
  width: calc(100% - 1.4rem);
  height: calc(100% - 1.4rem);
  object-fit: cover;
  object-position: top center;
  border-radius: 2.1rem;
  background: var(--n-900);
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
.phone .phone-video.is-ready {
  opacity: 1;
}
.phone-panel .phone .phone-video {
  filter: brightness(0.9);
}

/* Pinned hero (2026-09-02, owner request): while the clip plays, the page does not move past
   the hero — the section becomes a runway (one viewport plus --pin-runway of scroll), the card
   sticks at the frame inset, and site.js maps the runway progress to video.currentTime. The
   clip ends at 90 % of the runway so the last frame holds before the card releases. Only when
   site.js has actually activated a pinned clip (html.pin-js): no JS, reduced motion, data
   saver and phones all keep the ordinary hero. */
@media (min-width: 48rem) {
  html.pin-js .hero--frame[data-scroll-progress="pin"] {
    --pin-runway: 130vh;
    min-height: calc(100svh + var(--pin-runway));
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-frame {
    position: sticky;
    top: var(--frame-inset);
  }
}

/* ---------- 9.1c Hero: the cards arrive along the pinned runway ----------
   Poul 2026-09-02: "for at det ikke skal føles som man skal igennem videoen, burde vi kigge på
   de 3 knapper og reveal dem som man scroller? skal de komme ind fra højre step bar step? skal
   vi lave noget animation med de to app store knapper kommer 'kørende' ind fra venstre".

   The three cards step in from the right, one after the other, and the two store badges drive
   in from the left, each led by a small car glyph. On the badges that is Poul's explicit call:
   Apple's App Store Marketing Guidelines, "Badge Use", say "Don't modify, angle, or animate the
   App Store badge", and Google Play's are narrower ("don't remove or rearrange badge elements,
   or otherwise adjust the badge in any way"). The badge artwork itself is never redrawn,
   recoloured, rotated or embedded — it is translated and faded — and the car is a separate
   glyph beside it, which the guidelines do allow. Poul was shown the Apple sentence verbatim
   and asked for the drive-in anyway ("skulle vi ikke få dem til at køre ind som små biler
   også?"); it ships as his decision. The headline and the lead never move.

   Only inside the pinned runway (html.pin-js, ≥48rem): no JS, reduced motion, data saver and
   phones keep the hero exactly as it rests today, with everything visible from the first frame.
   --p defaults to 1 here, so a runway that never receives a value shows the finished hero. */
.badge-lane {
  position: relative;
}
/* the road only exists inside the pinned runway; everywhere else the badges are just badges */
.hero-road {
  display: none;
}
@media (min-width: 48rem) {
  /* Poul: "en slange der kører rundt". One winding road across the frame: a tarmac band with a
     dashed centre line on the same path, from the top corner, around the headline, back left and
     down past the badges. It is drawn along its own length by an SVG mask (a fat white stroke of
     the same path with the dash offset), so band and stripes appear together, following the
     curve — a clip or an inset would cut it in a straight line instead. */
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-road {
    display: block;
    position: absolute;
    inset: 0;
    /* explicit, not left to `inset: 0`: an <svg> is a replaced element with an intrinsic
       ratio (the viewBox is 1:1), so with height:auto the used height comes from the ratio
       and `bottom` is ignored — the road was 1384 px tall in an 844 px frame and its last
       40 % lived under the frame's overflow:clip, so the tail vanished at p ≈ .72. */
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
    z-index: 0;
    /* Poul: the road should not fade out — the tail should follow the head, and the last of it
       should leave the frame exactly as the hero releases and the next section takes over. So a
       fixed-length segment travels the path: the head runs 0 → len + seg across the whole runway,
       which puts the tail at len (the end of the road) at p = 1. */
    --seg: calc(var(--len, 4000px) * 0.42);
    /* +36px overshoot = half the mask's stroke-width: the mask path has a round linecap, so at
       head = len + seg the cap still protrudes back over the last 33 px of the path and leaves a
       brown lozenge in the corner for the rest of the scroll. Only visible over the decoded clip. */
    --head: calc((var(--len, 4000px) + var(--seg) + 36px) * var(--p, 1));
    /* out of the dark corner at the top */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%);
    mask-image: linear-gradient(to bottom, transparent, #000 14%);
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .road-band,
  html.pin-js .hero--frame[data-scroll-progress="pin"] .road-mid,
  html.pin-js .hero--frame[data-scroll-progress="pin"] .road-draw {
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
    /* the viewBox is stretched to the frame; without this the tarmac would be a wedge */
    vector-effect: non-scaling-stroke;
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .road-band {
    stroke: var(--brand-300);
    stroke-width: 54;
    opacity: 0.16;
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .road-mid {
    stroke: var(--brand-300);
    stroke-width: 2.5;
    stroke-dasharray: 18 16;
    opacity: 0.8;
  }
  /* the reveal itself: --len is the RENDERED length in px, written on the <svg> by site.js
     (non-scaling-stroke measures dashes in device pixels, not in the viewBox's user units, so
     getTotalLength() would draw a dashed ladder instead of a road). The 4000 px fallback is no
     longer "the whole road": with the travelling segment it makes --seg 1680 px on a ~1000-2000 px
     path, so an unmeasured road fills, then runs off the end around p .55. Degraded, not blank. */
  html.pin-js .hero--frame[data-scroll-progress="pin"] .road-draw {
    stroke: #fff;
    stroke-width: 66;
    /* one dash of --seg with a gap longer than the road, so only one segment is ever on it */
    stroke-dasharray: var(--seg) calc(var(--len, 4000px) * 1.2);
    stroke-dashoffset: calc(var(--seg) - var(--head));
  }
  /* the copy has to sit on top of the markings */
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-grid {
    position: relative;
    z-index: 1;
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .store-badges {
    position: relative;
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .store-badges a {
    --m: clamp(0, (var(--p, 1) - var(--b, 0)) / 0.14, 1);
    position: relative;
    opacity: var(--m);
    /* short enough that the car in front of the badge is inside the frame the whole way */
    transform: translateX(calc((1 - var(--m)) * -4.5rem));
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .store-badges a:nth-child(1) { --b: 0.02; }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .store-badges a:nth-child(2) { --b: 0.08; }
  /* the car that pulls the badge in: our own car.svg as a mask, so it is brand-300 line art
     and not a second colour. Brightest halfway through the drive (4 × m × (1 − m)), gone the
     moment the badge parks — nothing of it is left in the resting hero. */
  html.pin-js .hero--frame[data-scroll-progress="pin"] .store-badges a::before {
    content: "";
    position: absolute;
    top: 50%;
    right: calc(100% + 0.4rem);
    width: 1.6rem;
    height: 1.6rem;
    margin-top: -0.8rem;
    pointer-events: none;
    opacity: calc(4 * var(--m) * (1 - var(--m)));
    background: var(--brand-300);
    -webkit-mask: url("/assets/img/icons/car.svg") no-repeat center / contain;
    mask: url("/assets/img/icons/car.svg") no-repeat center / contain;
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-cards li {
    --u: clamp(0, (var(--p, 1) - var(--a, 0.18)) / 0.16, 1);
    opacity: var(--u);
    /* on the <li>, not the card: .hero-card:hover owns its own transform (the 2 px lift) */
    transform: translateX(calc((1 - var(--u)) * 3rem));
  }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-cards li:nth-child(1) { --a: 0.18; }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-cards li:nth-child(2) { --a: 0.27; }
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-cards li:nth-child(3) { --a: 0.36; }
  /* an item that is not there yet must not be tabbable or clickable */
  html.pin-js .hero--frame[data-scroll-progress="pin"] .hero-cards li[data-off],
  html.pin-js .hero--frame[data-scroll-progress="pin"] .store-badges a[data-off] {
    visibility: hidden;
  }
}

/* ---------- 9.1d The road continues into the features section (2026-09-03) ----------
   Poul: "instead of fading it out at the end of the scroll it just moves its tail to the head,
   and time it such that when the tail hits the border we start scrolling to the next section
   and the road continues onto that section doing a new path there."

   Two roads, one drive. The hero road's head runs 0 → len + seg over the hero runway, so its
   tail leaves the end of the path exactly at --p = 1 — the moment the hero frame's bottom edge
   reaches the viewport bottom and the features section's top edge reaches the viewport top…
   of the next screen. The handoff is exact because the second road runs on --q, not --p:

     --q = (viewportHeight − sectionTop) / sectionHeight   (site.js, [data-road-progress])

   which is 0 at the instant the features section's top edge touches the viewport BOTTOM — the
   same scroll position where the hero reaches --p = 1 (hero height = 100svh + 130vh, features
   follows with no margin, measured 2026-09-03) — and 1 where --p reaches 1. The section's own
   --p (0 only once the panel is pinned) would have left a full viewport of scroll with no road
   on screen at all.

   Where it can run: the runway panel fills the frame (1384 × 844 of a 1440 × 900 viewport,
   measured), so there is no ground around it — the section's gutter is 28 px. Inside it there
   is: the fitted grid is centred, which leaves a dark lane left of the phone column (184 px at
   1440, 104 px at 1280, 423 px at 1920, measured 2026-09-03). The second road is a narrower
   band down that lane, painted on the panel's own ground: above the scrubbed street clip
   (z-index −1) and below the windows, so the tiles break out over it and the last stretch —
   where the gaps go dark (--g) — leaves the road alone on black.

   Limitation, deliberately: the hero road needs html.pin-js (≥ 48rem), the continuation needs
   .is-runway (≥ 64rem and a tall enough viewport). Between 48 and 64rem the hero road drives
   and nothing continues it. Below that neither exists. */
.panel-road {
  display: none;
}
.section--features.is-runway .panel-road {
  display: block;
  position: absolute;
  inset: 0;
  /* explicit for the same reason as .hero-road: an <svg> is a replaced element with an
     intrinsic ratio, so `inset: 0` alone leaves a square and `bottom` is ignored */
  width: 100%;
  height: 100%;
  /* the band must stop at the panel's rounded edge, not spill onto the section ground */
  border-radius: var(--radius-frame);
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  --seg: calc(var(--len, 4000px) * 0.42);
  /* +28px: half the mask stroke-width, see the hero road above (round-cap nub at q = 1). */
  --head: calc((var(--len, 4000px) + var(--seg) + 28px) * var(--q, 1));
}
.section--features.is-runway .panel-road .road-band,
.section--features.is-runway .panel-road .road-mid,
.section--features.is-runway .panel-road .road-draw {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
/* narrower than the hero's 54: the lane is 104 px at 1280 and the band has to stay inside it */
.section--features.is-runway .panel-road .road-band {
  stroke: var(--brand-300);
  stroke-width: 40;
  opacity: 0.16;
}
.section--features.is-runway .panel-road .road-mid {
  stroke: var(--brand-300);
  stroke-width: 2;
  stroke-dasharray: 14 12;
  opacity: 0.8;
}
.section--features.is-runway .panel-road .road-draw {
  stroke: #fff;
  stroke-width: 52;
  stroke-dasharray: var(--seg) calc(var(--len, 4000px) * 1.2);
  stroke-dashoffset: calc(var(--seg) - var(--head));
}

/* ---------- 9.1e The road carries on down the page (2026-09-03) ----------
   Poul: "så skal den da bare fortsætte ned gennem Sådan virker det og køre bag ved Til jer,
   der allerede kender hinanden sectionen og komme frem igen ved I ved, hvem der er med, og
   så videre."

   That last clause is the whole design: the road is ONE element behind the entire lower page,
   not one per section. It is absolutely positioned against the initial containing block —
   <main>, <body> and <html> are all static, so `top` is a plain document coordinate — sized
   by site.js from the first section after the features panel down to the bottom of the last
   one, and painted at z-index -1. Which sections hide it is then a property of the page, not
   of this rule: "Sådan virker det" and the trust runway have a transparent background and let
   it through; the tinted band (#1a1614), the pricing band and the closing stage are opaque and
   the road simply disappears under them. Add or re-tint a section and the occlusion follows.

   It spans the full viewport width and winds down the left sixth of it (x 2–18 % of the
   viewBox). The gutter alone was tried first and rejected on the render: 144 px of lane over
   4800 px of page cannot bend enough to read as a road — it came out as a straight rail, the
   thing Poul rejected in the hero ("det ligner ikke engang en vej"). Full width buys real
   curvature, and at 1280, where the lane runs out, the road simply passes behind the step
   cards and shows again in the gaps — which is the same occlusion contract as the sections.
   site.js writes --road-top and --road-h in px.

   The reveal is a progressive draw, not the travelling segment of §9.1c/§9.1d: those two ride
   a pinned runway, where the frame stands still and the segment moves across it. This road is
   page-anchored, so anchoring a segment to the viewport would leave it parked at the same
   screen position for 4800 px of scroll — no perceived motion at all. Instead --r puts the
   drawing head just below the viewport's bottom edge (1.06 × innerHeight), so the road is
   simply *there* as it scrolls into view, and 0 falls exactly where the panel road's tail
   leaves (§9.1d). Deep-linking into the middle of the page therefore still draws it in rather
   than showing a finished road.

   ≥64rem and html.sp-js only: without JS the three geometry values are unset and the road
   would sit in the wrong place, so it stays display:none — as the other two roads do. */
.page-road {
  display: none;
}

@media (min-width: 64rem) {
  html.sp-js .page-road {
    display: block;
    position: absolute;
    top: var(--road-top, 0px);
    left: 0;
    right: 0;
    height: var(--road-h, 0px);
    z-index: -1;
    pointer-events: none;
    overflow: visible;
    --band: clamp(26px, 2.4vw, 40px);
    /* the lit stretch: the road is drawn in full and a soft window follows the scroll, so both
       ends fade into the dark instead of being cut off. Owner request 2026-09-03: "man kan ikke
       se snuden på vejen og halen hænger også uden for skærmen". site.js writes --win (0.9 ×
       viewport) and --win-top (document px, nose at 0.95 × viewport). A hard drawing head was
       what this replaced: parked below the bottom edge it is never visible, and moved up into
       the frame it leaves a blunt end mid-screen the moment you stop scrolling. */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 20%, #000 80%, transparent);
    mask-image: linear-gradient(to bottom, transparent, #000 20%, #000 80%, transparent);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% var(--win, 48rem);
    mask-size: 100% var(--win, 48rem);
    -webkit-mask-position: 0 var(--win-top, 0px);
    mask-position: 0 var(--win-top, 0px);
  }
  html.sp-js .page-road .road-band,
  html.sp-js .page-road .road-mid,
  html.sp-js .page-road .road-draw {
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
  }
  html.sp-js .page-road .road-band {
    stroke: var(--brand-300);
    stroke-width: var(--band);
    opacity: 0.16;
  }
  html.sp-js .page-road .road-mid {
    stroke: var(--brand-300);
    stroke-width: 2;
    stroke-dasharray: 14 12;
    /* the road itself is nearly vertical, so scrolling a page-anchored drawing past a pinned
       section (the trust runway) changes almost nothing on screen — it reads as frozen. The
       centre stripes crawl along the path with the scroll instead, ~0.3 px per px, which is
       the only motion cue that survives a pin. */
    stroke-dashoffset: calc(var(--len, 4000px) * var(--r, 0) * -0.3);
    opacity: 0.8;
  }
  html.sp-js .page-road .road-draw {
    stroke: #fff;
    stroke-width: calc(var(--band) + 12px);
    /* solid: the reveal is the CSS window above, not a dash */
  }
}

/* ---------- Trust section as S1 (2026-09-02) ----------
   Poul: "S1 kunne være en fed section". Shape from the mockup, claims stay ours: eyebrow
   pill, two-line headline with the accent phrase, four light plate items, store badges
   under the list, one big phone leaning right and leaving through the lower-right corner
   with the floor glow moved into that corner. The screen stays the real 03-bookings
   screenshot (light UI) — S1's dark UI would be generated UI, which is forbidden. */
.section-head .eyebrow--pill,
.eyebrow--pill {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.3rem 0.8rem 0.3rem 0.65rem;
  border: 1px solid hsl(22 82% 46% / 0.45);
  border-radius: 999px;
  background: hsl(22 82% 46% / 0.07);
  color: var(--brand-300);
  margin-bottom: var(--s-3);
}
.eyebrow--pill::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--brand-400);
  box-shadow: 0 0 12px hsl(22 82% 46% / 0.9);
}
.h2-accent {
  color: var(--accent-text);
}
.section-head--s1 h2 {
  max-width: 16ch;
}
.trust-list {
  gap: var(--s-3);
}
.trust-item--plate {
  align-items: center;
}
.trust-item--plate svg {
  width: 3.25rem;
  height: 3.25rem;
  padding: 0.85rem;
  border-radius: 14px;
}
.trust-item--plate h3 {
  font-size: var(--t-lg);
  margin-bottom: 0.1rem;
}
.trust-item--plate p {
  font-size: var(--t-sm);
}
.store-badges--trust {
  margin-top: var(--s-5);
}
.phone-stage--tilt .phone img {
  filter: brightness(0.9);
}
.phone-stage::before {
  inset: -10% -35% -45% -10%;
  background: radial-gradient(
    ellipse at 62% 78%,
    hsl(22 82% 46% / 0.68),
    hsl(22 82% 46% / 0.22) 40%,
    transparent 72%
  );
}
@media (min-width: 64rem) {
  .phone-stage {
    min-height: 40rem;
  }
  .phone-stage--tilt .phone-3d {
    left: 10%;
    top: 0;
    width: clamp(23rem, 34vw, 34rem);
    transform-origin: 50% 0;
    transform: rotate(var(--tilt-base, 8deg));
  }
  /* scroll-linked pose: --p = .5 is the static pose above (no-JS / reduced motion) */
  html.sp-js .phone-stage--tilt .phone-3d {
    transform: rotate(calc(var(--tilt-base, 8deg) + (0.5 - var(--p, 0.5)) * 5deg)) translateY(calc((0.5 - var(--p, 0.5)) * 6%));
  }
}
@media (max-width: 47.99rem) {
  /* both badges on one row under the list on a phone */
  .store-badges--trust {
    gap: 0.5rem;
  }
  .store-badges--trust img {
    height: 44px;
    width: auto;
  }
}
@media (min-width: 48rem) {
  /* S1: the accent phrase is the second line */
  .section-head--s1 .h2-accent {
    display: block;
  }
}

/* ---------- Store badges on dark, S1 style (2026-09-02) ----------
   Poul: the white plates looked "lidt grimme" next to S1. The black Apple/Google artwork
   with one hairline reads fine on n-950 (S1 does exactly this). Artwork never recoloured. */
.store-badges--dark {
  gap: var(--s-2);
}
.store-badges--dark a {
  display: inline-flex;
  border-radius: 8px;
  outline: 1px solid hsl(0 0% 100% / 0.22);
  outline-offset: -1px;
  transition: opacity 0.15s ease, outline-color 0.15s ease;
}
.store-badges--dark a:hover {
  outline-color: hsl(0 0% 100% / 0.45);
}
.store-badges--dark a:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}
.store-badges--dark img {
  display: block;
  height: 3rem;
  border-radius: 8px;
}

/* ---------- 3D phone (route A, 2026-09-02) ----------
   S1's device is a photoreal render turned a little so its right flank shows. Same idea in
   CSS, with the real screenshot untouched: the wrapper .phone-3d owns position + the
   scroll-linked --p motion (see the tilt rules above), the .phone inside owns the static 3D
   pose. rotateY(+) makes the right edge recede, and the ::after plate sitting 14px behind
   the body shows up along that receding edge as the metal flank. Nothing between the
   perspective root and the plate may clip (no overflow on .phone). */
.phone-3d {
  perspective: 1400px;
}
.phone-3d .phone {
  width: 100%;
  transform-style: preserve-3d;
  transform: rotateY(14deg) rotateX(2deg);
  border-color: hsl(0 0% 100% / 0.2);
  box-shadow:
    0 48px 80px -24px hsl(0 0% 0% / 0.7),
    inset 0 1px 0 hsl(0 0% 100% / 0.2),
    inset 0 -1px 0 hsl(0 0% 0% / 0.6);
}
.phone-3d .phone::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  transform: translateZ(-14px);
  background: linear-gradient(90deg, hsl(30 4% 18%), hsl(30 5% 46%) 55%, hsl(30 4% 16%));
}
.phone-3d .phone img,
.phone-3d .phone video {
  position: relative;
  transform: translateZ(1px);
}

/* ---------- 3D phone, route B: generated frame + the real screenshot (2026-09-02) ----------
   assets/img/phone/bookings-3d-{da,en}.webp = a gpt-image-2 render of an EMPTY titanium
   frame (screen = chroma green), keyed and filled with the real 03-bookings.webp by a PIL
   perspective warp (data/tmp/cc-www-mockups/phone-frame/composite.py; round-trip diff of
   the screen vs the source ≈ 0.8/255). The model never sees the UI. The render already
   leans ~6° clockwise, so the stage's base tilt drops to 1° (mobile 0°) via --tilt-base;
   the scroll-linked --p motion stays as it is. */
.phone-3d--render {
  --tilt-base: 0deg;
  perspective: none;
}
.phone-render {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 40px 60px hsl(0 0% 0% / 0.65));
}
@media (min-width: 64rem) {
  .phone-stage--tilt .phone-3d--render {
    --tilt-base: 1deg;
    left: 6%;
    width: clamp(25rem, 37vw, 37rem);
  }
}

/* ---------- 9.5d Trust: the clip loops inside the generated frame; phone sized by the row; S1 glow ----------
   Three layers share one 910×1784 canvas (tools/phone-frame/): the still composite at the bottom
   (fallback for no-JS, reduced motion, data saver, decode error — it also carries the drop shadow),
   the pre-warped booking clip in the middle, clipped to the screen quad so its black margins never
   show, and the frame with the screen punched to alpha on top (no filter here: a drop-shadow on the
   top layer would composite over the clip through the hole). The clip was warped with the same
   homography as the still (warp_video.py) — the app's own pixels, the model never saw them. */
.phone-3d--render {
  position: relative;
}
.phone-3d--render .phone-clip {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: fill;
  /* the screen's own soft mask from the frame (rounded corners) — a straight quad clip-path
     overshoots the corners and pokes out past the phone body where the top layer is transparent */
  -webkit-mask-image: url("/assets/img/phone/screen-mask.webp?v=2");
  mask-image: url("/assets/img/phone/screen-mask.webp?v=2");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  mask-mode: alpha;
  clip-path: polygon(12.402% 4.225%, 92.262% 1.448%, 83.273% 97.938%, 1.93% 96.748%);
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
.phone-3d--render .phone-clip.is-ready {
  opacity: 1;
}
.phone-3d--render .phone-render--empty {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  filter: none;
}
@media (prefers-reduced-motion: reduce) {
  .phone-3d--render .phone-clip,
  .phone-3d--render .phone-render--empty {
    display: none;
  }
}
/* S1's warm glow behind the phone: a soft radial, no blur filter (GPU cost on mobile), sitting
   between the section ground and the phone thanks to the stage's isolation. */
.phone-stage--tilt::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -10% -45% -30% -35%;
  background: radial-gradient(ellipse 56% 46% at 60% 66%, hsl(22 82% 46% / 0.62), hsl(22 82% 46% / 0.22) 46%, transparent 72%);
  /* fade out before the section's overflow clip so the glow never ends in a hard line */
  mask-image: linear-gradient(to bottom, #000 50%, transparent 92%);
  pointer-events: none;
}
/* More of the app on screen (S1 shows the whole screen down to the tab bar, chin cut off). */
.phone-stage--render {
  height: 39rem;
}
.phone-stage--render .phone-3d--render {
  /* canvas 910×1784 (0.51): 19rem wide = 37.3rem tall, which fills the 39rem stage; the old
     17.5rem was tuned for the narrower frame-01 canvas and left ~7rem dead stage below */
  width: min(19rem, 78%);
  transform-origin: 50% 100%;
}
/* The trust section is a full viewport on wide screens (S1 is a 16:9 canvas with the phone at
   its full height); the stage stretches to the row, and the phone is 113 % of it, so the chin
   leaves through the bottom edge and the screen is visible down to the tab bar. */
@media (min-width: 64rem) {
  .section--trust {
    display: grid;
    min-height: min(100svh, 62rem); /* tall-and-narrow (tablet portrait) must not grow the phone past the column */
  }
  .phone-stage--render {
    align-self: stretch;
    height: auto;
    min-height: 40rem;
    container-type: size; /* cqw/cqh below: the phone fits BOTH the row height and the column width */
  }
  /* Uniform scale only. The global img { max-width: 100% } used to cap the width at the column
     while height stayed 100 % of the row, which squashed the phone non-uniformly on wide screens
     (measured 0.39 instead of the canvas's 0.51 at 1920). The container carries the canvas
     aspect and picks the smaller of "115 % of the row height" and "90 % of the column". */
  .phone-stage--tilt .phone-3d--render {
    top: 1%;
    bottom: auto;
    left: 8%;
    /* 58.66cqh = 115cqh × 910/1784 (chin leaves through the bottom, like S1). The column is
       narrower than that at every common width, so the phone may run past it to the right —
       S1 bleeds its phone off the canvas edge too — and the section clips it at the viewport. */
    width: min(58.66cqh, 118cqw);
    height: auto;
    aspect-ratio: 910 / 1784;
  }
  .phone-3d--render .phone-render {
    width: 100%;
    height: auto;
    max-width: none;
    /* lighter than the base 40/60px shadow: that dark halo sat exactly where the glow has to show */
    filter: drop-shadow(0 24px 36px hsl(0 0% 0% / 0.5));
  }
  /* S1-scale glow. The base ellipse was tuned for the narrow frame-01 phone; with the true-proportion
     phone its core sat entirely behind the opaque still and the visible spill was ~3 % alpha. This one
     is wider than the column on both sides, centred on the phone's lower half, and holds ≥30 % alpha
     out to where it clears the phone's edges; the mask only fades the last fifth before the clip. */
  .phone-stage--tilt::before {
    inset: -30% -125% -25% -110%;
    background: radial-gradient(ellipse 46% 42% at 54% 72%, hsl(22 86% 50% / 0.95), hsl(22 84% 47% / 0.62) 30%, hsl(22 82% 46% / 0.3) 58%, hsl(22 82% 46% / 0.1) 78%, transparent 95%);
    mask-image: linear-gradient(to bottom, #000 80%, transparent 100%);
  }
}

/* ────────────────────────────────────────────────────────────────────────────────
   §11 Pressesiden (/presse, /en/press)

   Siden er til swag og tryk. Den skal vise mærket i de to sammenhænge, det faktisk
   sættes i — creme på mørk og orange på creme — og udlevere filerne. Alt herunder
   er klasser, ikke style-attributter: CSP'en har style-src 'self' + én hash og
   ingen 'unsafe-inline', så et style="" på en farveprøve ville blive blokeret og
   prøven stå usynlig. Det er den eneste fælde, der er speciel for denne side.
   ──────────────────────────────────────────────────────────────────────────────── */

/* Mærket vist stort. To flader, fordi mærket kun har de to baggrunde. */
.press-stage {
  display: grid;
  place-items: center;
  padding: clamp(2rem, 7vw, 4.5rem);
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
}
.press-stage img {
  width: min(22rem, 100%);
  height: auto;
}
.press-stage--night {
  background: var(--n-950);
}
.press-stage--cream {
  background: var(--brand-50);
  border-color: hsl(20 30% 60% / 0.35);
}
.press-stage--brand {
  background: var(--brand-500);
  border-color: hsl(20 40% 30% / 0.4);
}
.press-caption {
  margin-top: var(--s-1);
  color: var(--muted);
  font-size: 0.9375rem;
}

/* Downloads. En liste, ikke kort: filnavnet er det, folk leder efter. */
.press-files {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--border);
}
.press-files li {
  border-bottom: 1px solid var(--border);
}
.press-files a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 0.75rem;
  padding: 0.85rem 0.25rem;
  min-height: 2.75rem; /* 44px tap target */
  text-decoration: none;
  color: var(--fg);
}
.press-files a:hover,
.press-files a:focus-visible {
  color: var(--link-dark);
}
.press-files .file-name {
  flex: 1 1 12rem;
}
.press-files .file-meta {
  color: var(--muted);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}
/* Den samlede zip står øverst og må godt se ud som en handling. */
.press-files li:first-child a {
  font-weight: 600;
}

/* Farveprøver. Firkanten er en klasse pr. farve — se CSP-noten ovenfor. */
.press-swatches {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--s-2);
  list-style: none;
  margin: 0;
  padding: 0;
}
.press-swatch {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.press-swatch .chip {
  display: block;
  height: 5rem;
}
.press-swatch--brand .chip { background: var(--brand-500); }
.press-swatch--cream .chip { background: var(--brand-50); }
.press-swatch--night .chip { background: var(--n-950); }
.press-swatch .body {
  padding: 0.75rem;
}
.press-swatch b {
  display: block;
}
.press-swatch code {
  color: var(--muted);
  font-size: 0.875rem;
}
.press-swatch p {
  margin: 0.35rem 0 0;
  color: var(--muted);
  font-size: 0.9375rem;
}

/* Boilerplate til at kopiere. Monospace ville lyve om, at det er kode; det er tekst,
   der skal klippes ind i en artikel, så den står som tekst — bare markeret. */
.press-boiler {
  border-left: 3px solid var(--brand-500);
  padding-left: var(--s-2);
}
.press-boiler p + p {
  margin-top: 0.75rem;
}

/* Swag: tre fotos af tomme varer med mærket lagt på. */
.press-swag img {
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  display: block;
}

/* --- press hero (livery) -------------------------------------------------
   Pressesidens hero er husets egen: samme .hero--photo.hero--frame som
   /opladning, ikke et selvopfundet fuldbredde-bånd. De strukturelle regler
   deles gennem :is(.page-charging, .page-press) længere oppe; her står kun
   det, der er billedets eget — hvor i rammen bilen skal ligge.
   Folien sidder midt på bilen, så udsnittet holder om dørene og ikke om
   taget: 50% vandret, og lavere i båndet, jo bredere skærmen er, fordi det
   skalerede billede vokser hurtigere end båndets højde.
   --------------------------------------------------------------------- */
.page-press .hero-media img {
  object-position: 50% 58%;
}
@media (min-width: 64rem) {
  .page-press .hero-media img {
    object-position: 50% 72%;
  }
}
@media (min-width: 90rem) {
  .page-press .hero-media img {
    object-position: 50% 60%;
  }
}
/* Kortet møder næste bånd. Opladningssiden har .section--tint efter sin hero;
   her er det .section--tight, så afstanden sættes her. */
.page-press .hero--frame + .section {
  margin-top: var(--frame-inset);
}
