/* ──────────────────────────────────────────────────────────────
   Charles Marketing Website — Swiss International Typographic Style
   Dramatic scale contrast · heavy rules · flush-left editorial grid
   Design tokens mirror lib/core/theme/charles_theme.dart
   ────────────────────────────────────────────────────────────── */

:root {
  /* Colors — boosted contrast for screen readability */
  --bg: #FFFFFF;
  --text: #0A0A0A;                    /* near-pure black for maximum punch */
  --text-body: #2B2B2B;               /* body copy, higher contrast than the app's textMid */
  --text-dim: #6B6B6B;                /* labels, secondary info */
  --text-ghost: #BFBFBF;              /* placeholder text, tertiary */
  --rule: #E4E4E4;                    /* light dividers */
  --rule-heavy: #0A0A0A;              /* heavy section dividers */
  --surface: #F5F5F5;
  --charles-orange: #E8840E;

  /* The verified-review pill. Green is the one place the palette leaves orange,
     because a trust mark that matches the brand accent stops reading as one.
     Contrast is 5.6:1 dark-on-light and 9.8:1 light-on-dark. */
  --verified-bg: #E1F4E8;
  --verified-fg: #16713A;

  /* The screenshots are finished DEVICE RENDERS: the bezel, the rounded screen
     corners and the transparent surround are all baked into the PNG. So the
     frame draws no border and no radius of its own, or it would double the
     bezel and clip the render's own corners. The only thing left to style is
     the shadow, and it must be a drop-shadow filter so it follows the device
     silhouette instead of painting a rectangle behind a transparent image. */
  --phone-drop: drop-shadow(0 24px 34px rgba(0, 0, 0, 0.26));

  /* Scale — 8px grid */
  --brand-radius: 32px;
  --page-margin: clamp(24px, 6vw, 96px);
  --content-max: 1360px;

  /* Type */
  --font-heading: 'Space Grotesk', sans-serif;
  --font-mono: 'IBM Plex Mono', monospace;

  /* Layout rhythm */
  --section-gap-y: clamp(80px, 10vw, 160px);
  --section-gap-y-half: clamp(40px, 5vw, 80px);

  /* Measured height of the sticky header, published by interactions.js.
     The hero subtracts it so header + hero still equal one viewport. This
     value is only the pre-measurement fallback. */
  --header-h: 78px;
}

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

html {
  font-size: 16px;
  background: var(--bg);            /* overscroll + intro backdrop match the theme */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: 'kern' 1, 'ss01' 1;
  scroll-behavior: smooth;
  text-rendering: optimizeLegibility;
}

body {
  font-family: var(--font-heading);
  color: var(--text);
  background: var(--bg);
  line-height: 1.5;
}

img { max-width: 100%; height: auto; display: block; }
a { color: var(--text); }

/* ── Layout ─────────────────────────────────────────────────── */

.container {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 0 var(--page-margin);
}

.rule-heavy {
  width: 100%;
  max-width: var(--content-max);
  height: 3px;
  background: var(--rule-heavy);
  margin: 0 auto;
  position: relative;
  transform-origin: left;
}

/* Amber collar node at the start of each heavy rule (gym detail). Rides the
   phase 5 scaleX reveal without vertical squash: height is fixed, only X scales. */
.rule-heavy::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 14px;
  height: 14px;
  background: var(--charles-orange);
}

.rule {
  height: 1px;
  background: var(--rule);
  margin: 0;
}

/* ── Typography — Swiss editorial scale ─────────────────────── */

/* Display: the big hero headline. Huge on desktop, still big on mobile. */
.display {
  font-family: var(--font-heading);
  font-size: clamp(42px, 7.6vw, 104px);
  font-weight: 700;
  line-height: 0.94;
  letter-spacing: -0.035em;
  color: var(--text);
}

/* Section titles (one per section, flush-left) */
.section-title {
  font-family: var(--font-heading);
  font-size: clamp(40px, 6vw, 88px);
  font-weight: 700;
  line-height: 0.95;
  letter-spacing: -0.035em;
  color: var(--text);
}

/* Section number label: "01 / METHOD" — big mono, strong presence */
.section-num {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text);
}

/* Column / sub-section heading in mono label style */
.mono-label-big {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
}

/* Small mono label — for tiny metadata */
.mono-label {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Body lede — readable, stronger than the app's textMid */
.body-lede {
  font-family: var(--font-heading);
  font-size: clamp(17px, 1.4vw, 20px);
  font-weight: 400;
  line-height: 1.55;
  color: var(--text-body);
}

/* Subtitle / supporting line under a big headline */
.hero-lede {
  font-family: var(--font-heading);
  font-size: clamp(16px, 1.35vw, 20px);
  font-weight: 400;
  line-height: 1.4;
  color: var(--text-body);
  max-width: 46ch;
}

/* The founders' credential closes the lede rather than sitting in its own
   paragraph: two stacked blocks read as clutter in a hero clamped to one
   viewport. Bold is the only thing separating it from Charles's own words. */
.hero-lede strong {
  font-weight: 700;
}

/* ── Section scaffolding ────────────────────────────────────── */

section {
  padding: var(--section-gap-y) 0;
}

.section-header {
  display: flex;
  align-items: baseline;
  gap: 24px;
  margin-bottom: clamp(40px, 5vw, 80px);
}

.section-header .section-num {
  flex-shrink: 0;
}

.section-header .section-num::after {
  content: '';
  display: inline-block;
  width: clamp(24px, 3vw, 48px);
  height: 4px;
  background:
    repeating-linear-gradient(45deg,  var(--text) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(-45deg, var(--text) 0 1px, transparent 1px 4px);
  margin-left: 16px;
  vertical-align: middle;
}

/* ── Shader canvas ──────────────────────────────────────────── */

.shader {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
}

.shader--hero {
  /* Reach back up under the sticky header so the field still starts at the
     top of the viewport now that the bar sits outside .hero. */
  top: calc(-1 * var(--header-h));
  height: calc(100% + var(--header-h));
  /* fade the field out toward the edges so it sits behind, not on top */
  -webkit-mask-image: radial-gradient(135% 125% at 72% 48%, #000 22%, transparent 82%);
          mask-image: radial-gradient(135% 125% at 72% 48%, #000 22%, transparent 82%);
}

.shader--cta {
  -webkit-mask-image: radial-gradient(90% 120% at 50% 50%, #000 10%, transparent 70%);
          mask-image: radial-gradient(90% 120% at 50% 50%, #000 10%, transparent 70%);
  opacity: 0.9;
}

/* ── Scroll progress ────────────────────────────────────────── */

.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 100%;
  background: var(--charles-orange);
  transform: scaleX(0);
  transform-origin: left;
  z-index: 100;
  will-change: transform;
}

/* ── Motion pre-hide (anime.js drives the reveal) ───────────── */
/* Added by an inline head script only when modules are supported and
   motion is allowed; removed on reduced-motion, no-JS, module load
   failure (3s fail-open), or any init error, so content is never
   permanently hidden. */
html.motion-on .hero-motto,
html.motion-on .hero-lede,
html.motion-on .hero .badge-group,
html.motion-on .hero-phone,
html.motion-on .section-header,
html.motion-on .section-title,
html.motion-on .column,
html.motion-on .gallery-scroll,
html.motion-on .cta-title,
html.motion-on .cta-lede,
html.motion-on .cta .badge-group { opacity: 0; }

html.motion-on .rule-heavy { transform: scaleX(0); }

/* ── Intro film (injected by motion.js on every load) ───────────
   intro-pending pre-hides page content behind the coming overlay and
   locks scroll. Hiding body's CHILDREN (not body) keeps body in the
   a11y tree; the overlay and the consent card stay visible. All of it
   is bounded by the 3s boot timer and motion.js's watchdog.

   The overlay is a SCRIM, not a card: the hero's fog canvas is
   exempted below and runs underneath from the first frame, so the
   intro and the site share one continuous background and the exit is
   a plain fade. All type sizes and positions are measured and written
   by motion.js, never guessed. */
html.intro-pending { overflow: hidden; }
html.intro-pending body > :not(.intro-overlay) { visibility: hidden; }
/* The one exception: a descendant can re-enable visibility, so the fog
   stays live under the scrim and WebGL is warm before the handoff. */
html.intro-pending .shader--hero { visibility: visible; }

.intro-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;                 /* above the page (scroll-progress 100), below consent (10000) */
  background: color-mix(in srgb, var(--bg) 64%, transparent);
  cursor: pointer;
  overflow: hidden;
  will-change: opacity;
}

.intro-stage {
  position: relative;
  width: 100%;
  height: 100%;
  padding: 0 var(--page-margin);
}

/* "Strength is". Rendered at card size; motion.js parks it centred and
   tucks it into the top-left corner as a label with one transform. */
.intro-title {
  position: absolute;
  margin: 0;
  font-family: var(--font-heading);
  font-size: clamp(30px, 4.4vw, 62px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.1;
  color: var(--text);
  white-space: nowrap;
  opacity: 0;
  will-change: transform, opacity;
}

/* The reel. A full-height masked layer: words fade in near the bottom
   edge, pass the reading line crisp, and fade out toward the top, so
   the streaming uses the whole screen while nothing reserves layout.
   The spin blur lives here (never on the tall column inside), and the
   layer is kept only as wide as its widest word by motion.js. */
.intro-strip {
  position: absolute;
  top: 0;
  bottom: 0;
  pointer-events: none;
  will-change: transform, opacity, filter;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 5%, rgba(0, 0, 0, 0.45) 26%, #000 37%, #000 53%,
    rgba(0, 0, 0, 0.45) 68%, transparent 93%);
          mask-image: linear-gradient(to bottom,
    transparent 5%, rgba(0, 0, 0, 0.45) 26%, #000 37%, #000 53%,
    rgba(0, 0, 0, 0.45) 68%, transparent 93%);
}

/* The strip's one moving part. Deliberately NO will-change: the
   compositor should only rasterise the sliver the mask exposes. */
.intro-column {
  position: absolute;
  left: 0;
  right: 0;
}

/* One row of the reel. Height (the pitch) is written by motion.js. */
.intro-word {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin: 0;
}

.intro-word-t {
  font-family: var(--font-heading);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1;
  text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
}

/* Every passing word is text-colour; the payoff lands in the brand
   orange. The accent is reserved for the one word that stops. */
.intro-word--resolve .intro-word-t { color: var(--charles-orange); }

/* The endframe. Fitted to the content width by motion.js, so it is the
   biggest thing the screen has shown, on the same reading line. */
.intro-name {
  position: absolute;
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 0.92;
  color: var(--text);
  white-space: nowrap;
  opacity: 0;
  will-change: transform, opacity;
}

/* Fallback (a locale with no #intro-words island): the plain centred
   motto card, so translated pages keep today's behaviour untouched. */
.intro-fallback {
  position: absolute;
  left: 0;
  right: 0;
  text-align: center;
}
.intro-fallback .intro-motto {
  margin: 0 0 clamp(16px, 3vh, 38px);
  font-family: var(--font-heading);
  font-size: clamp(22px, 3.4vw, 44px);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.2;
  color: var(--charles-orange);
  opacity: 0;
}
.intro-fallback .intro-name {
  position: static;
  font-size: clamp(40px, 8vw, 110px);
}

/* ── Sticky header ──────────────────────────────────────────────
   The top bar is a body-level <header> rather than the first row of the
   hero, so position:sticky tracks the whole document instead of expiring
   with the hero's containing block. It keeps the floating-pill look: the
   glass bar sticks a short gap below the top edge. */

.site-header {
  position: sticky;
  top: 0;
  z-index: 90;                 /* above page content, below scroll-progress (100) */
  padding: clamp(12px, 2vh, 26px) 0 clamp(16px, 2.5vh, 28px);
}

.site-header > .container {
  position: relative;
  z-index: 1;
}

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

.hero {
  position: relative;
  min-height: calc(100vh - var(--header-h));
  min-height: calc(100svh - var(--header-h));
  display: flex;
  flex-direction: column;
  padding: 0 0 clamp(12px, 2vh, 26px);
}

.hero > .container {
  flex: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
}

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

/* The bar itself. Lives inside .site-header (not the hero) — the name is
   kept because it is still the top bar and every locale reuses it. */
.hero-top {
  position: relative;
  z-index: 30;                 /* the bar + its dropdown sit above the hero content */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.hero-brand {
  display: flex;
  align-items: center;
  gap: clamp(12px, 2vw, 20px);
}

.lang-switcher {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.lang-switcher a {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  color: var(--text-ghost);
  text-decoration: none;
  transition: color 0.15s ease;
}

.lang-switcher a:hover {
  color: var(--text);
}

.lang-switcher a.is-active {
  color: var(--charles-orange);
  font-weight: 600;
}

/* ── Header tools: Product Hunt + theme toggle + lang switcher ── */

.hero-top-tools {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2.4vw, 26px);
}

.hero-ph {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-body);
  text-decoration: none;
  border-bottom: 1px solid var(--charles-orange);
  padding-bottom: 2px;
  white-space: nowrap;
}

/* Product Hunt mark, sits before the link text everywhere it appears. */
.ph-logo {
  width: 15px;
  height: 15px;
  flex: none;
}
.hero-ph:hover { color: var(--text); }
@media (max-width: 900px) { .hero-ph { display: none; } }  /* keep the bar one line */

.theme-toggle {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-body);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 4px 10px;
  min-width: 9ch;                 /* fits OTOMATIK (TR) so cycling never reflows */
  text-align: center;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.theme-toggle:hover { color: var(--text); border-color: var(--text-dim); }
.theme-toggle:focus-visible {
  outline: 2px solid var(--charles-orange);
  outline-offset: 2px;
}

/* ── Language dropdown (compact chip + panel) ───────────────── */
.lang-select { position: relative; }

.lang-current {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-body);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 4px 10px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.lang-current:hover { color: var(--text); border-color: var(--text-dim); }
.lang-current:focus-visible { outline: 2px solid var(--charles-orange); outline-offset: 2px; }

.lang-caret {
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-1px) rotate(45deg);
  transition: transform 0.2s ease;
}
.lang-current[aria-expanded="true"] .lang-caret { transform: translateY(1px) rotate(-135deg); }

.lang-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  min-width: 150px;
  padding: 6px;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 12px;
  box-shadow: 0 14px 34px rgba(10, 10, 10, 0.14);
}
.lang-menu[hidden] { display: none; }
.lang-menu a {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-body);
  text-decoration: none;
  padding: 8px 12px;
  border-radius: 8px;
  transition: background 0.12s ease, color 0.12s ease;
}
.lang-menu a:hover,
.lang-menu a:focus-visible {
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text);
  outline: none;
}
.lang-menu a.is-active { color: var(--charles-orange); }

.glass--bar .lang-current { color: var(--text-body); border-color: color-mix(in srgb, var(--text) 25%, transparent); }
.glass--bar .lang-current:hover { color: var(--text); }

/* ── Liquid glass (Apple-style, palette-neutral via tokens) ─────
   Background/border use color-mix on --bg/--text so dark mode flips
   automatically. NO rgba() literals for background/border; the shadow
   rgba is fine because a missing shadow degrades safely. */

:root {
  --glass-blur: 18px;
  --glass-saturate: 160%;
}

.glass {
  background: var(--bg);
  background: color-mix(in srgb, var(--bg) 92%, transparent);   /* no-blur fallback: near-solid */
  border: 1px solid var(--rule);
  border: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
  border-radius: var(--brand-radius);
  box-shadow:
    0 12px 32px rgba(10, 10, 10, 0.10),
    inset 0 1px 0 rgba(255, 255, 255, 0.25);   /* specular top edge */
}

@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .glass {
    background: color-mix(in srgb, var(--bg) 62%, transparent);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
            backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  }
}

.glass--bar {
  border-radius: calc(var(--brand-radius) / 2);
  padding: 8px clamp(14px, 2vw, 20px);
}
.glass--card { padding: clamp(9px, 1.2vh, 13px) clamp(16px, 2.5vw, 26px); }
.glass--strip {
  display: inline-block;
  border-radius: calc(var(--brand-radius) / 2);
  padding: 10px 18px;
}

/* On glass, text is --text or --text-body only; grays and orange become
   markers and rules. .cta-facts carries mono-label AND glass on one element,
   so match same-element and descendant. */
.glass.mono-label, .glass .mono-label { color: var(--text-body); }
.glass .badge-qr-label { color: var(--text); }
.glass .badge-coming-soon { color: var(--text); display: flex; align-items: center; gap: 8px; }
.glass .badge-coming-soon::before {
  content: ""; width: 8px; height: 8px; flex: none; background: var(--charles-orange);
}
.glass--bar .lang-switcher a { color: var(--text-body); }
.glass--bar .lang-switcher a:hover { color: var(--text); }
.glass--bar .lang-switcher a.is-active { color: var(--charles-orange); }
.glass--bar .theme-toggle { color: var(--text-body); border-color: color-mix(in srgb, var(--text) 25%, transparent); }
.glass--bar .theme-toggle:hover { color: var(--text); }

/* Accessibility: solid on request. Stays AFTER @supports; also repeats the
   no-glass kill-switch selector so a perf downgrade cannot re-introduce
   translucency for someone who asked for solid surfaces. */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .glass,
  html.no-glass .glass {
    background: var(--bg);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-color: var(--text-dim);
  }
}
@media (forced-colors: active) {
  .glass { background: Canvas; -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* Perf kill switch: near-solid, no blur (still AA in both modes). */
html.no-glass .glass {
  background: var(--bg);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

@media (max-width: 900px) { :root { --glass-blur: 12px; } }

.hero-logo {
  width: 44px;
  height: 44px;
}

.hero-num {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text);
}

.hero-grid {
  flex: 1;
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: clamp(32px, 5vw, 80px);
  align-items: center;
}

.hero-text {
  display: flex;
  flex-direction: column;
}

/* Cased in CSS, not in the markup, so each locale uppercases under its own
   rules (Turkish i -> İ) and the accents survive. */
.hero-motto {
  font-family: var(--font-heading);
  font-size: clamp(16px, 1.6vw, 20px);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--charles-orange);
  margin-bottom: clamp(10px, 1.5vh, 16px);
}

.hero-title {
  font-size: clamp(40px, 6.8vw, 92px);
  margin-bottom: clamp(16px, 2vh, 28px);
}

/* French headline words run long; smaller type keeps the three-line lockup. */
[lang="fr"] .hero-title {
  font-size: clamp(34px, 5.8vw, 80px);
}

.hero-lede {
  margin-bottom: clamp(12px, 1.5vh, 20px);
}

.hero-phone {
  display: flex;
  justify-content: flex-end;
  perspective: 1000px;
}

.hero-phone .phone-frame {
  height: min(606px, 70vh);
  width: auto;
  aspect-ratio: 438 / 886;
  will-change: transform;
  transform: translateZ(0);
  transition: filter 0.3s ease;
}

.hero-phone .phone-frame:hover {
  filter: drop-shadow(0 30px 46px rgba(232, 132, 14, 0.38));
}

/* ── Phone frame ────────────────────────────────────────────── */

.phone-frame {
  width: 300px;              /* 606 * 438/886, the render's own aspect */
  height: 606px;
  position: relative;
  filter: var(--phone-drop);
}

.phone-frame img {
  width: 100%;
  height: 100%;
  object-fit: contain;       /* never crop a device render */
  display: block;
}

/* The hero phone runs OPPOSITE to the page: a dark screen on the light site, a
   light screen on the dark site, so the phone reads as an object sitting on the
   page instead of dissolving into it. Both files ship and CSS picks one, because
   the theme has three states (auto/light/dark) and <picture media> cannot see
   the data-theme attribute. The gallery phones deliberately alternate light and
   dark regardless of theme, so they carry neither class. */
/* Hero phone, theme crossfade.
   Both skins are stacked and faded between; neither is ever display:none.
   Two reasons, and the first one is why this broke once already:
     1. `display` is not animatable, so a swap can only ever be a hard cut.
     2. A `display` rule on .phone-screen--on-dark (0,1,0) LOSES to
        `.phone-frame img { display: block }` (0,1,1), so in light mode both
        images rendered and the second spilled out of the frame.
   Opacity is animatable and nothing else in the sheet sets opacity on these,
   so there is no specificity race left to lose. */
.hero-phone .phone-frame > .phone-screen {
  position: absolute;
  inset: 0;
  transition: opacity 420ms ease;
}

.hero-phone .phone-screen--on-light { opacity: 1; }
.hero-phone .phone-screen--on-dark  { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .hero-phone .phone-frame > .phone-screen { transition: none; }
}

.phone-frame-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  position: relative;
}

.phone-frame-placeholder::before {
  content: '';
  position: absolute;
  inset: 20px;
  border: 1px solid var(--rule);
}

.phone-frame-placeholder span {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-ghost);
  position: relative;
  z-index: 1;
}

/* ── Columns (method + process) ─────────────────────────────── */

.columns-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(32px, 4vw, 64px);
  position: relative;
}

.column {
  position: relative;
  padding-top: 24px;
}

/* Knurled rule at the top of each column (gym detail) */
.column::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background:
    repeating-linear-gradient(45deg,  var(--text) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(-45deg, var(--text) 0 1px, transparent 1px 4px);
}

.column-number {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  color: var(--text-dim);
  margin-bottom: 20px;
}

.column-label {
  margin-bottom: 20px;
}

/* ── Voices ─────────────────────────────────────────────────── */
/* Testimonial marquee straight under the hero. This is the one warm,
   soft-edged block on an otherwise hard flush-left grid, and that contrast
   is the point: it should read as people talking, not as a claim panel.
   It drifts on its own, slowly, and pauses when you hover to actually read
   one. No JS: the loop is the duplicated second run of cards. */

/* No heavy rules bracket this any more, so the padding is what separates it
   from the hero above and the problem section below. It must be symmetric:
   the strip floats, and uneven air around it reads as a mistake. */
.voices {
  padding: clamp(56px, 6.5vw, 100px) 0;
}

/* The problem section's own top padding would stack on top of the strip's
   bottom padding and make the gap below roughly four times the gap above.
   The strip's padding is doing that job here, so drop the section's. */
.voices + .problem {
  padding-top: 0;
}

.voices-scroll {
  overflow: hidden;
  /* Fade both ends so cards enter and leave instead of being chopped. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 4%, #000 96%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 4%, #000 96%, transparent 100%);
  padding: 18px 0;
}

.voices-track {
  display: flex;
  align-items: stretch;
  gap: clamp(16px, 1.8vw, 26px);
  width: max-content;
  animation: voices-drift 78s linear infinite;
}

.voices-scroll:hover .voices-track,
.voices-scroll:focus-within .voices-track {
  animation-play-state: paused;
}

@keyframes voices-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

.voice-card {
  flex: 0 0 clamp(270px, 25vw, 340px);
  box-sizing: border-box;
  min-height: 238px;
  background: var(--surface);
  border-radius: 20px;
  padding: 22px 24px 24px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
}

/* Header row: photo, then the name with its star rating stacked beside it.
   The quote moved to the bottom so a card reads the way an App Store review
   does, identity first. */
.voice-by {
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 100%;
}

.voice-id {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  min-width: 0;
}

/* Member photo. Stock portraits, not the reviewers themselves: see the note in
   the VOICES markup. The orange fill stays as the background so a failed image
   degrades to a solid circle rather than a broken-image glyph. */
.voice-avatar {
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: 50%;
  object-fit: cover;
  background: var(--charles-orange);
  display: block;
  /* A hairline, drawn outside the box so it costs no layout. Several of the
     portraits are shot against near-black, and without this the circle
     dissolves into the card in dark mode. */
  box-shadow: 0 0 0 1px var(--text-ghost);
}

.voice-name {
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Per review, never an average: Marcus B. rated four, the other six rated five. */
.voice-stars {
  color: var(--charles-orange);
  font-size: 11px;
  letter-spacing: 1.5px;
  line-height: 1;
}

/* The badge says "App Store review". It must never say "verified buyer".
   The quote is a real, checkable App Store review; the face beside it is a
   stock portrait of someone who has never used Charles, so the wording is
   allowed to vouch for the review and never for the person. Full reasoning in
   docs/VOICE-AVATARS.md.

   It sits on its own row rather than under the name, because the German and
   Portuguese strings are far too long for the narrow column left over beside
   a 44px avatar on a 270px card. */
.voice-verified {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  padding: 3px 9px 3px 7px;
  border-radius: 999px;
  background: var(--verified-bg);
  color: var(--verified-fg);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.05em;
  line-height: 1.4;
  text-transform: uppercase;
}

.voice-verified::before {
  content: '\2713';
  flex: none;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
}

.voice-quote {
  font-family: var(--font-heading);
  font-size: 16px;
  line-height: 1.55;
  color: var(--text-body);
  text-wrap: pretty;
}

.voice-quote::before { content: '\201C'; }
.voice-quote::after  { content: '\201D'; }

@media (prefers-reduced-motion: reduce) {
  .voices-scroll { overflow-x: auto; }
  .voices-track { animation: none; }
  .voice-card { transform: none; }
}

/* ── Hero laurels ───────────────────────────────────────────────
   The badge pair beside the QR. Artwork is assets/laurel.svg (founder
   supplied, traced), applied as a mask so it takes currentColor and follows
   the theme. Re-trace from source if it changes; never hand-edit the paths. */

.hero-laurels {
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.2vw, 18px);
}

/* The wreath's opening sits above the middle of its box: the arms curve up
   and the stems cross low, so the lower third is solid artwork. Centring the
   label in the box therefore looks like it has sunk. Padding the bottom of
   the flex box lifts the content into the actual opening, and because it is a
   percentage it holds at every badge size. The mask is inset:0 on ::before,
   so the wreath itself is unaffected. */
.laurel-badge {
  position: relative;
  width: clamp(94px, 9.2vw, 114px);
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-bottom: 8%;
  color: var(--text);
}

.laurel-badge::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  -webkit-mask: url(/assets/laurel.svg) center / contain no-repeat;
  mask: url(/assets/laurel.svg) center / contain no-repeat;
}

/* The wreath narrows top and bottom, so the text box has to stay well
   inside its width or the longest line rides over the leaves. */
.laurel-inner {
  position: relative;
  max-width: 56%;
  font-family: var(--font-mono);
  font-size: clamp(7px, 0.62vw, 8.5px);
  font-weight: 600;
  line-height: 1.45;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  text-align: center;
}

.laurel-stars {
  position: relative;
  color: var(--charles-orange);
  font-size: clamp(13px, 1.25vw, 16px);
  letter-spacing: 1px;
}

/* Below the QR breakpoint the badges would crowd the buttons, and the whole
   install block is already hidden there. */
@media (max-width: 900px) {
  .hero-laurels { display: none; }
}

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

.problem-copy {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(32px, 4vw, 64px);
  max-width: 1100px;
}

.problem-lede {
  font-family: var(--font-heading);
  font-size: clamp(19px, 1.7vw, 26px);
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: -0.01em;
  color: var(--text-body);
}

@media (max-width: 900px) {
  .problem-copy {
    grid-template-columns: 1fr;
    gap: 28px;
  }
}

/* ── Screenshot gallery ─────────────────────────────────────── */

.gallery {
  padding: var(--section-gap-y) 0;
}

/* Slow auto-scrolling marquee, pure CSS. The track holds two identical sets
   of screenshots, so translating it exactly -50% loops seamlessly. It pauses
   on hover so you can read a screen, and falls back to plain manual scroll
   when the visitor prefers reduced motion. A soft edge mask fades the sides. */
.gallery-scroll {
  overflow: hidden;
  padding: clamp(24px, 4vw, 48px) 0 8px;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 5%, #000 95%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 5%, #000 95%, transparent 100%);
}

.gallery-track {
  display: flex;
  gap: clamp(20px, 3vw, 48px);
  width: max-content;
  animation: gallery-marquee 90s linear infinite;
}
.gallery-scroll:hover .gallery-track { animation-play-state: paused; }

@keyframes gallery-marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .gallery-scroll { overflow-x: auto; }
  .gallery-track { animation: none; }
}

.gallery-item {
  flex: 0 0 260px;
}


.gallery-item .phone-frame {
  width: 240px;
  height: 486px;             /* 240 / (438/886) */
  filter: none;              /* the marquee stays shadowless, as before */
}

/* ── Feedback / Talk to us ──────────────────────────────────── */

.feedback {
  padding: var(--section-gap-y) 0;
}

.feedback-copy {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(32px, 4vw, 64px);
  max-width: 1100px;
}

@media (max-width: 900px) {
  .feedback-copy {
    grid-template-columns: 1fr;
    gap: 28px;
  }
}

/* Standard primary button: orange pill, black label, one subtle lift on
   hover. No decorative shadow. This is the ONE button style on the site. */
.btn-primary {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #0A0A0A;
  background: var(--charles-orange);
  padding: 16px 34px;
  border-radius: 999px;
  text-decoration: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.btn-primary:hover {
  opacity: 0.9;
  transform: translateY(-1px);
}

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

.cta {
  position: relative;
  padding: clamp(52px, 7vw, 104px) 0;
  text-align: center;
}

.cta .container {
  position: relative;
  z-index: 1;
}

.cta .section-header {
  justify-content: center;
}

.cta-title {
  font-family: var(--font-heading);
  font-size: clamp(34px, 5vw, 68px);
  font-weight: 700;
  line-height: 0.94;
  letter-spacing: -0.03em;
  margin-bottom: 22px;
  max-width: 16ch;
  margin-left: auto;
  margin-right: auto;
}

.cta-contrast {
  font-family: var(--font-heading);
  font-size: clamp(20px, 2vw, 30px);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.02em;
  color: var(--text);
  margin-bottom: 16px;
  max-width: 22ch;
  margin-left: auto;
  margin-right: auto;
}

.cta-contrast strong,
.cta-price {
  color: var(--charles-orange);
}

.cta-lede {
  font-family: var(--font-heading);
  font-size: clamp(17px, 1.3vw, 19px);
  font-weight: 400;
  line-height: 1.55;
  color: var(--text-body);
  margin-bottom: 48px;
  max-width: 40ch;
  margin-left: auto;
  margin-right: auto;
}

/* ── CTA facts strip + Product Hunt link ────────────────────── */

.cta-facts {
  max-width: 62ch;
  margin: 0 auto clamp(22px, 3vw, 32px);
  font-size: 13px;
  text-transform: none;
  letter-spacing: 0.04em;
  line-height: 1.7;
  color: var(--text-dim);
}

.cta-ph {
  display: inline-flex;
  align-items: center;
  margin-top: clamp(16px, 2.2vw, 26px);
  text-transform: none;
  letter-spacing: 0.04em;
  text-decoration: none;
  color: var(--text-dim);
  border-bottom: 1px solid var(--charles-orange);
  padding-bottom: 2px;
}

.cta-ph:hover { color: var(--text); }

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

.faq-caption {
  max-width: 60ch;
  margin-top: clamp(14px, 2vw, 22px);
  font-size: 13px;
  color: var(--text-dim);
  text-transform: none;
  letter-spacing: 0;
}

.faq-list {
  border-top: 2px solid var(--rule-heavy);
}

.faq-item {
  border-bottom: 2px solid var(--rule-heavy);
}

.faq-item > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: clamp(14px, 2vw, 28px);
  padding: clamp(9px, 1.15vw, 14px) 0;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: clamp(15px, 1.5vw, 18px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text);
}

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

.faq-item > summary::after {
  content: "+";
  flex: none;
  font-family: var(--font-mono);
  font-weight: 400;
  color: var(--charles-orange);
  transition: transform .2s ease;
}

.faq-item[open] > summary::after { transform: rotate(45deg); }

.faq-item > p {
  margin: 0 0 clamp(14px, 2vw, 20px);
  max-width: 66ch;
  color: var(--text-body);
  line-height: 1.55;
}

/* FAQ is the most content-dense section; lighter vertical padding than the
   generic section rhythm so the longer list still holds close to one viewport. */
.faq { padding: clamp(44px, 5vw, 80px) 0 clamp(72px, 8vw, 120px); }

/* Two columns on laptop and up: the list flows row-major, each row's rule
   stays aligned across both columns, so ~20 items read as one screen. */
/* Two independent accordion columns: opening an item in one column never
   affects the other (fixes the linked-row gap). Group 1 is training
   questions people search, group 2 is what Charles does. */
.faq-groups {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(28px, 4vw, 64px);
  margin-top: clamp(22px, 3vw, 38px);
}
.faq-group-label {
  color: var(--charles-orange);
  margin-bottom: clamp(10px, 1.3vw, 14px);
}
.faq-item > p { max-width: 60ch; }

@media (max-width: 860px) {
  .faq-groups { grid-template-columns: 1fr; gap: clamp(22px, 5vw, 36px); }
}

/* ── App Store badge ────────────────────────────────────────── */

/* Two columns: the label sits over the badges on the left, the scan-to-install
   QR sits alongside on the right. Side by side rather than stacked so the hero
   still fits within one viewport. Collapses to one column below 900px, where
   the QR is hidden. */
.badge-group {
  display: grid;
  grid-template-columns: max-content max-content;
  column-gap: clamp(20px, 2.4vw, 30px);
  row-gap: 12px;
  align-items: center;
  justify-items: start;
  justify-content: start;
  width: fit-content;          /* the install card hugs its content, no dead space */
}

.badge-group .badge-coming-soon {
  grid-area: 1 / 1;
}

.badge-group .badge-row {
  grid-area: 2 / 1;
}

.badge-group .badge-qr {
  grid-area: 1 / 2 / span 2 / auto;
}

.badge-group--centered {
  justify-items: center;
  justify-content: center;
  margin-left: auto;
  margin-right: auto;
}

/* Hero variant: no card. Two store badges stacked on the left, the QR to
   their right with a caption underneath. Sits straight on the page — the
   CTA copy keeps its glass card because there it sits on a shader. */
.badge-group--bare {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: clamp(22px, 2.6vw, 36px);
  margin-top: clamp(10px, 1.8vh, 22px);
}

/* The badges stack vertically, aligned to a shared width so their edges
   line up cleanly. */
.badge-group--bare .badge-row {
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  flex-wrap: nowrap;
}

.badge-group--bare .appstore-badge {
  height: auto;
  width: clamp(150px, 15vw, 178px);
}

/* The QR sits beside the badges with its caption centred underneath. The
   caption is wider than the code and must stay on one line; without this it
   wraps now that the laurel badges share the row. */
.badge-group--bare .badge-qr {
  align-items: center;
  gap: 10px;
}

.badge-group--bare .badge-qr-label {
  white-space: nowrap;
}


.badge-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.badge-group--centered .badge-row {
  justify-content: center;
}

.badge-coming-soon {
  color: var(--charles-orange);
  font-weight: 600;
  letter-spacing: 0.18em;
}

.appstore-badge {
  height: 52px;
  width: auto;
  display: block;
}

.appstore-link {
  display: inline-block;
  transition: opacity 0.15s ease;
}

.appstore-link:hover {
  opacity: 0.85;
}

.appstore-badge--disabled {
  opacity: 0.45;
  filter: grayscale(1);
  cursor: not-allowed;
  user-select: none;
  pointer-events: none;
}

/* ── Scan-to-install QR ─────────────────────────────────────── */

/* Desktop only: a QR cannot be scanned by the device displaying it, so it is
   hidden below 900px where the store badges are already tappable. */
.badge-qr {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

.badge-group--centered .badge-qr {
  align-items: center;
}

/* The SVG is self-contained by design: brand-cream plate, rounded corners
   and the ISO 18004 four-module quiet zone are baked into the asset, so it
   stays scannable wherever it lands (both sections put it over an animated
   shader, where an opaque light plate is a decode requirement, not taste).
   The border-radius mirrors the SVG's own corner (3 of 37 modules, ~8%) so
   shadows and outlines follow the drawn shape. */
.badge-qr-code {
  width: 132px;
  height: 132px;
  border-radius: 8%;
  flex-shrink: 0;
}

.badge-group--bare .badge-qr-code {
  width: 118px;
  height: 118px;
}

.badge-qr-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
}

.badge-group--centered .badge-qr-text {
  text-align: center;
}

.badge-qr-label {
  color: var(--text);
  font-weight: 600;
}

/* ── Footer ─────────────────────────────────────────────────── */

.footer {
  padding: 48px 0 64px;
  border-top: 1px solid var(--rule);
  margin-top: 0;
}

.footer-motto {
  font-family: var(--font-heading);
  font-size: clamp(34px, 5vw, 72px);
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 0.98;
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: 28px;
}

.footer-motto .motto-outline {
  color: transparent;
  -webkit-text-stroke: 1.5px var(--text);
}

.footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}

.footer-company {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0.14em;
  color: var(--text-dim);
  text-transform: uppercase;
}

.footer-links {
  display: flex;
  gap: 32px;
  flex-wrap: wrap;
}

.footer-links a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--text);
  text-transform: uppercase;
  text-decoration: none;
  position: relative;
  padding-bottom: 2px;
}

.footer-links .ph-logo {
  flex: none;
}

.footer-links a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--text);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 200ms ease-out;
}

.footer-links a:hover::after {
  transform: scaleX(1);
}

/* ── Responsive ─────────────────────────────────────────────── */

@media (max-width: 900px) {
  .hero-grid {
    grid-template-columns: 1fr;
    gap: 48px;
  }

  .hero-phone {
    justify-content: center;
    padding-top: 0;
  }

  .hero-lede {
    max-width: 100%;
  }

  .columns-grid {
    grid-template-columns: 1fr;
    gap: 48px;
  }

  .section-header {
    flex-direction: column;
    gap: 16px;
    align-items: flex-start;
  }

  /* Nobody can scan a code with the phone that is showing it. */
  .badge-qr {
    display: none;
  }

  /* Collapse the now-empty QR column so its gap does not offset the badges. */
  .badge-group {
    grid-template-columns: max-content;
    column-gap: 0;
  }
}

@media (max-width: 600px) {
  .phone-frame {
    width: 257px;            /* 520 * 438/886 */
    height: 520px;
  }

  .gallery-item {
    flex: 0 0 200px;
  }

  .gallery-item .phone-frame {
    width: 200px;
    height: 405px;           /* 200 / (438/886) */
  }

  .hero-top {
    margin-bottom: 48px;
  }

  .footer-inner {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* ── Legal pages ────────────────────────────────────────────── */

/* Same trap as .roadmap below: this rule follows .container, so the
   horizontal padding has to be restated or the gutter is lost and the
   text runs to the screen edge on phones. */
.legal-content {
  padding: 48px var(--page-margin) 96px;
  max-width: calc(720px + var(--page-margin) * 2);   /* 720px of measure, plus the gutter */
}

.legal-content h1 {
  font-family: var(--font-heading);
  font-size: clamp(32px, 4vw, 48px);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1.0;
  margin-bottom: 12px;
}

.legal-content h2 {
  font-family: var(--font-heading);
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-top: 48px;
  margin-bottom: 16px;
  color: var(--text);
}

.legal-content h3 {
  font-family: var(--font-heading);
  font-size: 18px;
  font-weight: 700;
  margin-top: 28px;
  margin-bottom: 10px;
  color: var(--text);
}

.legal-content p {
  font-size: 16px;
  line-height: 1.75;
  color: var(--text-body);
  margin-bottom: 18px;
}

.legal-content ul, .legal-content ol {
  padding-left: 24px;
  margin-bottom: 18px;
}

.legal-content li {
  font-size: 16px;
  line-height: 1.75;
  color: var(--text-body);
  margin-bottom: 6px;
}

.legal-content strong {
  color: var(--text);
  font-weight: 600;
}

.legal-content a {
  color: var(--charles-orange);
}

.legal-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-decoration: none;
  margin-bottom: 48px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--text-dim);
}

.legal-back:hover {
  color: var(--text);
  border-bottom-color: var(--text);
}

/* ── Roadmap ────────────────────────────────────────────────────
   Three columns, always all three, side by side. The page answers one
   question at a glance: what is moving, what is next, what is done.

   The earlier version stacked the three states vertically and hid any
   that were empty, so the page read as one endless list of identical
   2px-ruled rows. Here the heavy rule appears once per column, under its
   label; rows are separated by hairlines. Shipped rows carry a title and
   a month only, which is what keeps the column short enough to sit
   beside the other two. */

/* Horizontal padding is restated, not dropped: this rule comes after
   .container in the sheet, so a `padding: 40px 0 96px` shorthand would
   zero out the page gutter and let text run to the screen edge on
   phones, where .container's max-width is not doing the job. */
.roadmap {
  padding: 40px var(--page-margin) 96px;
}

.roadmap .legal-back {
  margin-bottom: 32px;
}

.roadmap-head {
  max-width: 60ch;
  margin-bottom: clamp(36px, 4.2vw, 60px);
}

.roadmap h1 {
  font-family: var(--font-heading);
  font-size: clamp(40px, 5.2vw, 72px);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 0.95;
  color: var(--text);
  margin-bottom: 16px;
}

.roadmap-lede {
  font-family: var(--font-heading);
  font-size: clamp(16px, 1.35vw, 19px);
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-body);
  margin-bottom: 20px;
}

.roadmap .roadmap-updated {
  color: var(--text-dim);
  margin: 0;
}

.roadmap-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 3.4vw, 56px);
  align-items: start;
}

.roadmap-group {
  padding: 0;
}

.roadmap-group-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-bottom: 12px;
  border-bottom: 2px solid var(--rule-heavy);
}

/* The site's crosshatch tick is noise at column width; the status square
   does that job here and carries meaning as well. */
.roadmap-group-head .section-num::after { content: none; }

.roadmap-marker {
  flex-shrink: 0;
  width: 9px;
  height: 9px;
}

.roadmap-group[data-status="in_progress"] .roadmap-marker { background: var(--charles-orange); }
.roadmap-group[data-status="planned"] .roadmap-marker { box-shadow: inset 0 0 0 1.5px var(--text-dim); }
.roadmap-group[data-status="shipped"] .roadmap-marker { background: var(--text); }

.roadmap .roadmap-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.roadmap .roadmap-item {
  padding: 14px 0;
  margin: 0;
  border-bottom: 1px solid var(--rule);
}

.roadmap-item-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.roadmap .roadmap-item-title {
  font-family: var(--font-heading);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--text);
  margin: 0;
}

.roadmap .roadmap-item-date {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.roadmap .roadmap-item-body {
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--text-body);
  margin: 6px 0 0;
}

.roadmap .roadmap-empty {
  list-style: none;
  padding: 14px 0;
  margin: 0;
  border-bottom: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-ghost);
}

.roadmap .roadmap-state {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 48px 0 0;
}

@media (max-width: 900px) {
  .roadmap-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 48px;
  }
}

/* ── Accessibility overrides for the art layer ──────────────── */

.nav-fab { display: none; }

/* ── CTA store buttons ──────────────────────────────────────────
   Section 09 is deliberately bare: two store badges under the lede and
   nothing else. No glass card, no label, no QR, no Product Hunt line. The
   hero install block keeps all of those; this one is the last word on the
   page and earns its weight by being quiet. */

.cta-stores {
  justify-content: center;
  gap: clamp(14px, 1.6vw, 20px);
  margin-top: clamp(30px, 4vw, 52px);
}

.cta-stores .appstore-badge {
  height: 52px;
  width: auto;
}

@media (max-width: 600px) {
  .cta-stores {
    flex-direction: column;
    align-items: center;
    gap: 14px;
  }

  .cta-stores .appstore-badge {
    height: auto;
    width: min(232px, 68vw);
  }
}

/* ── Mobile hardening ───────────────────────────────────────────
   Phone-specific fixes, audited at 390x844. Everything here exists because
   it was measurably broken on a phone, not as a precaution. */

@media (max-width: 600px) {
  /* The bar wrapped "01 / CHARLES" onto two lines and doubled its own
     height. The section number is chrome, not information, so it goes and
     the wordmark stays on one line. */
  .hero-num {
    white-space: nowrap;
    font-size: 12px;
  }

  .hero-brand {
    gap: 10px;
    min-width: 0;
  }

  .hero-top {
    gap: 10px;
  }

  .hero-top-tools {
    gap: 8px;
    flex-shrink: 0;
  }

  /* THE horizontal scroll on mobile: the CTA install card held both store
     badges side by side, which is wider than a phone, so the card pushed
     ~11px past the viewport and the whole document scrolled sideways. */
  .badge-group--centered {
    max-width: 100%;
  }

  .badge-group--centered .badge-row {
    flex-direction: column;
    align-items: center;
    gap: 12px;
    width: 100%;
  }

  .badge-group--centered .appstore-badge {
    height: auto;
    width: min(232px, 68vw);
  }

  /* Belt and braces: nothing in the CTA may exceed the viewport again. */
  .cta .container {
    overflow-x: clip;
  }

  /* Testimonials: cards were 270px wide in a 390px viewport and the marquee
     carried them off before they could be read. On a phone it becomes a
     swipeable snap carousel instead: one card at a time, no auto motion, the
     reader controls it. */
  .voices-track {
    animation: none;
    width: auto;
    scroll-snap-type: x mandatory;
  }

  .voices-scroll {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-inline: var(--page-margin);
    /* The edge fade hides the swipe affordance once it is scrollable. */
    -webkit-mask-image: none;
            mask-image: none;
  }

  .voices-scroll::-webkit-scrollbar {
    display: none;
  }

  .voice-card {
    flex: 0 0 82vw;
    scroll-snap-align: center;
    transform: none;
    padding: 22px;
  }

  /* All sets stay in the DOM on mobile: setupVoicesLoop() parks the reader in
     the middle set and shifts by exactly one set at the edges, so the swipe
     never reaches an end. */

  /* Footer: the motto broke across lines mid-phrase, the company line ran to
     three, and the Product Hunt logo sat on a line of its own. */
  .footer-motto {
    font-size: clamp(28px, 9vw, 40px);
    line-height: 1.05;
  }

  .footer-company {
    font-size: 11px;
    line-height: 1.6;
  }

  .footer-links {
    flex-wrap: wrap;
    gap: 14px 20px;
  }

  .footer-links a {
    gap: 7px;
    white-space: nowrap;
  }

  /* The floating privacy button sat on top of the footer links. */
  .footer {
    padding-bottom: 96px;
  }

  /* Vertical rhythm. The page runs ~10.8 phone screens, and the biggest
     single contributor is not copy, it is air: --section-gap-y bottoms out at
     80px, so every section spends 160px on padding alone. The three-column
     sections then stack with a 48px gap between each. Tightened here only;
     desktop rhythm is untouched. No copy is cut. */
  :root {
    --section-gap-y: 56px;
    --section-gap-y-half: 32px;
  }

  .columns-grid {
    gap: 34px;
  }

  .section-header {
    margin-bottom: 28px;
  }

  /* The bar collapses to one round button once past the hero, and taps back
     open. Both states occupy the same slot and cross-fade; the header keeps
     its height either way, so --header-h never changes and the hero does not
     reflow underneath. Motion matches the site: a short deliberate ease, no
     bounce, no spring. */
  .site-header .container {
    position: relative;
  }

  .nav-fab {
    position: absolute;
    top: 0;
    right: var(--page-margin);
    width: 52px;
    height: 52px;
    padding: 0;
    border: none;
    border-radius: 50%;
    display: grid;
    place-items: center;
    cursor: pointer;
    opacity: 0;
    transform: scale(0.86);
    pointer-events: none;
    transition:
      opacity 0.26s cubic-bezier(0.4, 0, 0.2, 1),
      transform 0.26s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .nav-fab-logo {
    width: 26px;
    height: 26px;
  }

  .hero-top {
    transform-origin: top right;
    transition:
      opacity 0.26s cubic-bezier(0.4, 0, 0.2, 1),
      transform 0.26s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .site-header.is-compact .hero-top {
    opacity: 0;
    transform: scale(0.92);
    pointer-events: none;
  }

  .site-header.is-compact .nav-fab {
    opacity: 1;
    transform: scale(1);
    pointer-events: auto;
  }

  .site-header.is-compact.is-open .hero-top {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }

  .site-header.is-compact.is-open .nav-fab {
    opacity: 0;
    transform: scale(0.86);
    pointer-events: none;
  }

  /* 05 / HOW is cut on phones: three stacked steps for ~900px, saying what
     the method section above already said. The numbering then reads
     02,03,04,06 on mobile, which is a known and accepted seam. */
  .process {
    display: none;
  }
}

@media (prefers-contrast: more) {
  .footer-motto .motto-outline { color: var(--text); -webkit-text-stroke: 0; }
}
@media (forced-colors: active) {
  .footer-motto .motto-outline { color: CanvasText; -webkit-text-stroke: 0; }
}

/* ── Dark mode (follows the system, orange/white/black only) ── */

/* ── Dark mode (orange/white/black only) ──────────────────────
   The same payload appears twice with two different switches:
   1. System prefers dark AND the user has not forced light.
   2. The user forced dark, regardless of system.
   EDITING RULE: keep the two payloads byte-identical, always. Any
   dark-mode change is made in BOTH blocks below. */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #0A0A0A;
    --text: #FFFFFF;
    --text-body: #D6D6D6;              /* 12.6:1 on #0A0A0A */
    --text-dim: #9C9C9C;               /* 6.9:1 on #0A0A0A */
    --text-ghost: #4A4A4A;
    --rule: #262626;
    --rule-heavy: #FFFFFF;
    --surface: #161616;
    --verified-bg: rgba(74, 222, 128, 0.13);
    --verified-fg: #56DE8C;
    --phone-drop: drop-shadow(0 24px 34px rgba(0, 0, 0, 0.7));
  }
  :root:not([data-theme="light"]) .hero-logo { filter: invert(1); }
  :root:not([data-theme="light"]) .btn-primary { color: #0A0A0A; }
  :root:not([data-theme="light"]) .hero-phone .phone-screen--on-light { opacity: 0; }
  :root:not([data-theme="light"]) .hero-phone .phone-screen--on-dark { opacity: 1; }
  :root:not([data-theme="light"]) .appstore-badge { outline: 1px solid var(--text-dim); outline-offset: -1px; border-radius: 9px; }
  :root:not([data-theme="light"]) .hero-lede,
  :root:not([data-theme="light"]) .cta-lede { color: var(--text); }
  :root:not([data-theme="light"]) .cta-contrast strong,
  :root:not([data-theme="light"]) .cta-price { color: var(--text); }
  :root:not([data-theme="light"]) .glass {
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.08);
  }
}

/* Manual dark: identical payload, attribute switch. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0A0A0A;
  --text: #FFFFFF;
  --text-body: #D6D6D6;
  --text-dim: #9C9C9C;
  --text-ghost: #4A4A4A;
  --rule: #262626;
  --rule-heavy: #FFFFFF;
  --surface: #161616;
  --verified-bg: rgba(74, 222, 128, 0.13);
  --verified-fg: #56DE8C;
  --phone-drop: drop-shadow(0 24px 34px rgba(0, 0, 0, 0.7));
}
:root[data-theme="dark"] .hero-logo { filter: invert(1); }
:root[data-theme="dark"] .btn-primary { color: #0A0A0A; }
:root[data-theme="dark"] .hero-phone .phone-screen--on-light { opacity: 0; }
:root[data-theme="dark"] .hero-phone .phone-screen--on-dark { opacity: 1; }
:root[data-theme="dark"] .appstore-badge { outline: 1px solid var(--text-dim); outline-offset: -1px; border-radius: 9px; }
:root[data-theme="dark"] .hero-lede,
:root[data-theme="dark"] .cta-lede { color: var(--text); }
:root[data-theme="dark"] .cta-contrast strong,
:root[data-theme="dark"] .cta-price { color: var(--text); }
:root[data-theme="dark"] .glass {
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}

/* Manual light under a dark system: keep UA form controls light too. */
:root[data-theme="light"] { color-scheme: light; }
