/* ============================================================================
   KALV — landing page design system.

   PORTED FROM skiv-web/assets/css/main.css on 2026-08-28, on the founder's
   call: "tu vas copier la landing page, tout le wireframe, tout le design de
   la landing page de Skiv". The skeleton, the proportions, the topbar, the
   phone carousel, the store pills, the legal bar and every breakpoint are
   Skiv's, declaration for declaration.

   WHAT CHANGED, and only this: the palette values. Same token names, same
   roles, but the ink is Kalv's own (#0E0E0D) and the card is the app's own
   canvas (#F5F5F3, Kalv-X/Design/Theme.swift). Two products cannot share a
   brand; they can share a layout.

   TYPE stays Skiv's system stack. Kalv is a SwiftUI app drawn in SF Pro, so
   the web is closer to the product here than the Archivo pass it replaces.

   SCOPE. This file dresses / only. The satellite routes (/faq/, /blog/,
   /contact/, /legal/*) still run on shell.css + page.css — the previous
   design system, frozen so this port could land without breaking them. They
   are ported route by route, and shell.css dies with the last one.
   ========================================================================= */

:root {
  --paper: #ffffff;
  --paper-soft: #f0f0ee;
  /* A transition to a keyword size (`auto`, `min-content`) is a no-op without
     this. The FAQ answers declare a height transition that simply never ran:
     the opacity faded and the height snapped. It is declared here, on the
     document, because it is a document-level switch — a route stylesheet
     reaching `:root` would be the same violation as one reaching the chrome. */
  interpolate-size: allow-keywords;

  --ink: #0e0e0d;
  --ink-2: #5c5c58;
  /* 2026-08-30: was #84847f, which measures 3.44:1 on the #F5F5F3 card and
     fails WCAG AA for small text — and it carries the whole legal bar on every
     page, the article meta, the ledger dates and the form's error message.
     #6e6e69 is 4.70:1, the nearest value that passes, so the ramp shifts as
     little as it can while the footer becomes legible. */
  --ink-3: #6e6e69;
  --line: rgba(14, 14, 13, 0.1);
  /* The separator INSIDE a bordered surface. It has to sit below `--line` or a
     panel's own edge stops out-ranking the hairlines it contains — measured
     2026-09-01, the two were 5.19 and 4.85 ΔL* against their grounds, a ratio
     of 1.07, which is no hierarchy at all. faq.css named this token in a
     comment for weeks without anyone declaring it. */
  --line-2: rgba(14, 14, 13, 0.05);
  --stage: #f5f5f3;
  --stage-text: #0e0e0d;
  --stage-muted: #5c5c58;
  --stage-line: rgba(14, 14, 13, 0.1);
  --cta-apple-bg: #0e0e0d;
  --cta-apple-bg-hover: #242422;
  --cta-apple-fg: #ffffff;
  --header-control-height: 56px;
  /* THE CARD'S RADIUS AND THE HEADER'S INSET ARE ONE NUMBER (2026-09-01).
     They used to be two unrelated clamps — the card at clamp(24px, 2.3vw, 38px)
     and the bar's top at a flat 28px — so the top inset was ALWAYS smaller than
     the radius. Measured at 1920 the corner arc spans y=10..48 while the header
     box starts at y=39: the logo and the App Store pill began inside the arc
     while it was still turning. Nothing collided; it simply read as broken.
     Founder: *"les corners en haut"*. Bind them and the bar clears the curve at
     every width by construction. */
  --frame-radius: clamp(24px, 2.3vw, 38px);
  --footer-bar: 42px;
  --ease-ui: cubic-bezier(0.22, 0.72, 0, 1);
  --ease-carousel: cubic-bezier(0.32, 0.72, 0, 1);
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif;
  /* SaaS restyle 2026-08-19: display type joins the grotesque stack.
     The New York serif era is in git history. */
  --font-display: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", sans-serif;
  --font-label: "SFMono-Regular", "SF Mono", Menlo, Monaco, monospace;
}

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

html {
  min-width: 320px;
  min-height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-x: none;
  background: var(--paper);
  color: var(--ink);
  -webkit-text-size-adjust: 100%;
}

body {
  width: 100%;
  min-height: 100%;
  margin: 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-x: none;
  background: var(--paper);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

button,
a { color: inherit; }

a { text-decoration: none; }
button { font: inherit; }

.landing {
  min-height: 100vh;
  min-height: 100svh;
}

.site-frame {
  height: 100vh;
  height: 100svh;
  display: grid;
  grid-template-rows: minmax(0, 1fr) var(--footer-bar);
  background: var(--paper);
}

.hero {
  position: relative;
  min-height: 0;
  margin: 10px 10px 0;
  overflow: hidden;
  isolation: isolate;
  border: 1px solid rgba(14, 14, 13, 0.08);
  border-radius: var(--frame-radius);
  background: var(--stage);
}

/* The ambient repeats the card's own background on its own layer. It is
   absolutely positioned at inset 0 with a SQUARE box, so the moment the hero
   stops clipping — the article route (`.doc-scroll`) at every width, and every
   satellite route below 900px — the square paints grey over all four of the
   card's rounded corners and the card reads as a rectangle with a stray arc
   drawn on top. `inherit` makes the fill follow the card's radius instead of
   depending on an ancestor's clip. (Founder, 2026-08-30: "quand on rentre dans
   les articles, les bordures en haut et en bas ne sont pas arrondies".) */
.hero__ambient {
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background: var(--stage);
}

.hero__ambient::after {
  content: none;
}

.topbar {
  position: absolute;
  z-index: 20;
  top: var(--frame-radius);
  left: var(--frame-radius);
  right: var(--frame-radius);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.brand {
  display: inline-grid;
  width: var(--header-control-height);
  height: var(--header-control-height);
  flex: 0 0 var(--header-control-height);
  place-items: center;
}

/* 66px in a 56px box made `place-items: center` fall back to start, so all ten
   pixels of overflow went right and DOWN: the glove painted 5px below the nav
   and the App Store pill on every route, and escaped the card wherever the hero
   stops clipping. The art carries 3.5% of internal padding, so at 56px it still
   paints 52.7px and stays the heaviest object in the bar. */
.brand__mark {
  display: block;
  width: var(--header-control-height);
  height: var(--header-control-height);
  object-fit: contain;
}

.site-nav {
  position: absolute;
  left: 50%;
  display: flex;
  align-items: center;
  height: var(--header-control-height);
  min-height: var(--header-control-height);
  padding: 7px 10px;
  transform: translateX(-50%);
  border: 0;
  background: none;
  box-shadow: none;
}

.site-nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 42px;
  padding: 0 20px;
  color: var(--ink-2);
  font-size: 0.84rem;
  font-weight: 650;
  letter-spacing: 0.005em;
  transition: color 180ms var(--ease-ui);
}

.site-nav a::after {
  content: "";
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 11px;
  height: 2px;
  border-radius: 99px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 240ms var(--ease-ui);
}

.site-nav a:hover,
.site-nav a[aria-current="page"] { color: var(--stage-text); }
.site-nav a:hover::after,
.site-nav a:focus-visible::after,
.site-nav a[aria-current="page"]::after { transform: scaleX(1); }

.brand:focus-visible,
.site-nav a:focus-visible,
.header-cta:focus-visible,
.mobile-nav summary:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* `relative`, so the panel below anchors to the BURGER. Without it `right: 0`
   resolved against the padded sticky bar and the panel pinned to the card's
   inner edge — 14px past the burger on a phone, and 142-164px past it in the
   744-1024 band where the bar's contents ride an inboard column. */
.mobile-nav { display: none; position: relative; }
.mobile-nav summary { list-style: none; cursor: pointer; }
.mobile-nav summary::-webkit-details-marker { display: none; }

.mobile-nav summary {
  position: relative;
  display: grid;
  width: 46px;
  min-height: 46px;
  place-items: center;
}

/* Three bars, drawn from the CENTRE of the box out (top/left 50% + a half-bar
   margin), never pinned to absolute offsets. The previous version placed each
   bar with its own `top`, and the mobile block then re-pinned only the first
   and the last — which dropped the top bar to 18px and lifted the bottom one
   to 25px while the middle stayed at 22.5px. Two bars 2.5px apart read as one
   thick bar: that is the broken icon. Centre-out geometry cannot be half
   overridden, so the glyph survives any breakpoint that resizes the box.

   Open, the outer two rotate onto the middle line and the middle one fades.
   The translate and the rotate ride the SAME transform in the same order, so
   the browser interpolates function by function — one motion, no matrix
   decomposition, no reflow. (Rebuilt 2026-08-30, founder.) */
.mobile-nav summary span {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 2px;
  margin: -1px 0 0 -10px;
  border-radius: 99px;
  background: var(--stage-text);
  transition: transform 200ms var(--ease-ui), opacity 120ms var(--ease-ui);
}

.mobile-nav summary span:nth-child(1) { transform: translateY(-6px) rotate(0deg); }
.mobile-nav summary span:nth-child(2) { transform: translateY(0) rotate(0deg); }
.mobile-nav summary span:nth-child(3) { transform: translateY(6px) rotate(0deg); }

.mobile-nav[open] summary span:nth-child(1) { transform: translateY(0) rotate(45deg); }
.mobile-nav[open] summary span:nth-child(2) { opacity: 0; transform: translateY(0) rotate(0deg) scaleX(0.4); }
.mobile-nav[open] summary span:nth-child(3) { transform: translateY(0) rotate(-45deg); }

.mobile-nav__panel {
  position: absolute;
  top: 56px;
  right: 0;
  z-index: 30;
  display: grid;
  width: min(250px, calc(100vw - 28px));
  padding: 9px;
  border: 1px solid var(--line);
  border-radius: 24px;
  /* Opaque, not glass. A translucent panel lets the headline read straight
     through the menu items — verified in the render, 2026-08-30. Glass is for
     chrome that sits over its own page; a menu is a surface you read. */
  background: var(--paper);
  box-shadow: 0 18px 40px -14px rgba(14, 14, 13, 0.28);
  text-align: left;
}

.mobile-nav__panel a {
  padding: 14px 15px;
  border-radius: 16px;
  color: var(--stage-muted);
  font-size: 0.88rem;
  font-weight: 650;
}

.mobile-nav__panel a:hover { background: rgba(14, 14, 13, 0.055); color: var(--stage-text); }
.mobile-nav__panel a[aria-current="page"] {
  background: rgba(14, 14, 13, 0.055);
  color: var(--stage-text);
}

.hero__copy {
  position: absolute;
  z-index: 5;
  top: 52%;
  left: clamp(40px, 5vw, 82px);
  width: min(44vw, 700px);
  transform: translateY(-45%);
}

.callout {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 clamp(18px, 1.65vw, 25px);
  color: var(--stage-text);
  font-family: var(--font-label);
  font-size: clamp(0.68rem, 0.68vw, 0.8rem);
  font-weight: 600;
  letter-spacing: 0.065em;
  line-height: 1.2;
  text-transform: uppercase;
  white-space: nowrap;
}

.callout span {
  width: 26px;
  height: 2px;
  border-radius: 999px;
  background: var(--ink);
}

h1 {
  max-width: none;
  margin: 0;
  color: var(--stage-text);
  font-family: var(--font-display);
  font-size: clamp(3.1rem, 4.3vw, 5.2rem);
  font-feature-settings: "kern" 1, "liga" 1;
  font-weight: 650;
  letter-spacing: -0.045em;
  line-height: 0.9;
  text-rendering: optimizeLegibility;
}

.headline-line {
  display: block;
  width: max-content;
  white-space: nowrap;
}

.hero__sub {
  max-width: none;
  margin: clamp(23px, 1.8vw, 28px) 0 clamp(25px, 2vw, 31px);
  color: var(--stage-muted);
  font-size: clamp(0.88rem, 0.96vw, 1.06rem);
  font-weight: 460;
  letter-spacing: -0.015em;
  line-height: 1.45;
}

.subheadline-line {
  display: block;
  white-space: nowrap;
}

.store-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.store-cta {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  min-height: 58px;
  padding: 0 23px 0 19px;
  border: 0;
  border-radius: 999px;
  background: var(--cta-apple-bg);
  color: var(--cta-apple-fg);
  text-align: left;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 10px 24px -14px rgba(0, 0, 0, 0.35);
  transition: transform 220ms var(--ease-ui), box-shadow 220ms var(--ease-ui), background-color 220ms var(--ease-ui);
}

.store-cta--soon {
  border: 1px solid rgba(14, 14, 13, 0.1);
  background: rgba(255, 255, 255, 0.58);
  color: rgba(14, 14, 13, 0.46);
  box-shadow: none;
  cursor: not-allowed;
}

.store-cta--soon svg { fill: currentColor; stroke: none; }

.store-cta--apple,
.store-cta--apple:link,
.store-cta--apple:visited,
.store-cta--apple:hover,
.store-cta--apple:active,
.store-cta--apple:focus-visible {
  border-color: transparent;
  background-color: #0e0e0d;
  color: #ffffff;
}

.store-cta--apple:hover { background-color: #242422; }
.store-cta--apple svg,
.store-cta--apple small,
.store-cta--apple strong { color: #ffffff; fill: #ffffff; }

.store-cta:hover {
  transform: translateY(-2px);
  background: var(--cta-apple-bg-hover);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 14px 28px -14px rgba(0, 0, 0, 0.4);
}

.store-cta--soon:hover { transform: none; background: rgba(255, 255, 255, 0.58); box-shadow: none; }
.store-cta:active { transform: scale(0.98); }

/* Disabled = the App Store control while the app has no listing. It keeps the
   black --apple treatment (it is the page's focal mark) but must not lift on
   hover or offer a pointer: a control that animates like a link and goes
   nowhere is a worse lie than a grey one. */
.store-cta:disabled { cursor: not-allowed; }
.store-cta:disabled:hover,
.store-cta--apple:disabled:hover {
  transform: none;
  background-color: #0e0e0d;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 10px 24px -14px rgba(0, 0, 0, 0.35);
}
.store-cta:focus-visible { outline: 3px solid var(--ink); outline-offset: 4px; }
.store-cta svg { flex: none; }
.store-cta span { display: grid; }
.store-cta small { font-size: 0.6rem; line-height: 1.05; white-space: nowrap; }
.store-cta strong { font-size: 1.1rem; line-height: 1.05; letter-spacing: -0.025em; white-space: nowrap; }

.header-cta.store-cta {
  width: auto;
  height: var(--header-control-height);
  min-height: var(--header-control-height);
  gap: 10.62px;
  padding: 0 22.21px 0 18.34px;
  background: var(--cta-apple-bg);
  color: var(--cta-apple-fg);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 8px 20px -12px rgba(0, 0, 0, 0.3);
}

.header-cta.store-cta:hover { background: var(--cta-apple-bg-hover); }
.header-cta.store-cta svg { width: 21.24px; height: 25.1px; }
.header-cta.store-cta small { font-size: 0.579rem; }
.header-cta.store-cta strong { font-size: 1.062rem; }

.product-showcase {
  position: absolute;
  z-index: 4;
  top: clamp(102px, 8.6vw, 142px);
  right: clamp(12px, 2.1vw, 34px);
  bottom: clamp(18px, 1.8vw, 30px);
  left: 50%;
  min-width: 0;
  touch-action: pan-y;
}

.carousel-stage {
  position: absolute;
  inset: clamp(112px, 12vh, 142px) 0 clamp(92px, 10vh, 118px);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%);
}

.phone-carousel {
  position: absolute;
  inset: 0;
  perspective: 1100px;
}

.iphone-mockup {
  position: absolute;
  top: 50%;
  left: 50%;
  height: min(88%, 565px);
  aspect-ratio: 640 / 1408;
  overflow: visible;
  opacity: 0;
  transform: translate3d(-50%, -54%, 0) scale(0.72);
  filter: blur(5px) saturate(0.8);
  pointer-events: none;
  transition:
    transform 520ms var(--ease-carousel),
    opacity 280ms var(--ease-ui),
    filter 420ms var(--ease-ui);
  will-change: transform, opacity, filter;
}

.iphone-shell {
  position: absolute;
  inset: 0;
  overflow: visible;
  filter:
    drop-shadow(0 20px 22px rgba(14, 14, 13, 0.14))
    drop-shadow(0 6px 9px rgba(14, 14, 13, 0.09));
}

.iphone-frame {
  position: absolute;
  z-index: 1;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  user-select: none;
}

.iphone-mockup.is-active {
  z-index: 4;
  opacity: 1;
  transform: translate3d(-50%, -57%, 0) scale(1);
  filter: blur(0) saturate(1);
}

.iphone-mockup.is-prev,
.iphone-mockup.is-next {
  z-index: 2;
  opacity: 0.68;
  filter: blur(0.4px) saturate(0.9);
}

.iphone-mockup.is-prev {
  transform: translate3d(-118%, -54%, -70px) scale(0.78) rotate(-3.5deg);
}

.iphone-mockup.is-next {
  transform: translate3d(18%, -54%, -70px) scale(0.78) rotate(3.5deg);
}

.iphone-mockup.is-hidden {
  z-index: 1;
  opacity: 0;
  transform: translate3d(82%, -50%, -130px) scale(0.68) rotate(6deg);
}

.product-showcase.is-instant .iphone-mockup,
.product-showcase.is-instant .feature-copy {
  transition: none !important;
}

.product-screen {
  position: absolute;
  z-index: 2;
  inset: 4.8% 7.3% 3.2%;
  overflow: hidden;
  border-radius: 13% / 6%;
  background: transparent;
}

.product-screen--empty {
  display: grid;
  place-items: center;
  color: rgba(14, 14, 13, 0.42);
  font-family: var(--font-label);
  font-size: clamp(0.58rem, 0.66vw, 0.76rem);
  font-weight: 600;
  letter-spacing: 0.08em;
}

.feature-copy-stack {
  position: absolute;
  z-index: 8;
  top: 0;
  right: 8%;
  left: 8%;
  height: clamp(96px, 10.5vh, 126px);
  text-align: center;
}

.feature-copy {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  color: var(--stage-text);
  pointer-events: none;
  opacity: 0;
  transform: translateY(10px);
  filter: blur(4px);
  transition: transform 380ms var(--ease-carousel), opacity 220ms var(--ease-ui), filter 300ms var(--ease-ui);
}

.feature-copy p {
  margin: 0 0 10px;
  color: var(--ink-2);
  font-size: 0.64rem;
  font-weight: 760;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.feature-copy h2 {
  margin: 0 0 8px;
  font-size: clamp(1.55rem, 2vw, 2.25rem);
  font-weight: 690;
  letter-spacing: -0.052em;
  line-height: 1;
}

.feature-copy small {
  display: block;
  color: var(--stage-muted);
  max-width: 42ch;
  font-size: clamp(0.74rem, 0.78vw, 0.88rem);
  line-height: 1.42;
}

.feature-copy.is-active {
  pointer-events: auto;
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
  transition-delay: 80ms;
}

.carousel-controls {
  position: absolute;
  z-index: 10;
  right: 0;
  bottom: clamp(64px, 7vh, 84px);
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.carousel-arrows {
  display: flex;
  gap: 10px;
  pointer-events: auto;
}

.carousel-arrows button {
  position: relative;
  isolation: isolate;
  display: grid;
  width: 48px;
  height: 48px;
  overflow: hidden;
  padding: 0;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(14, 14, 13, 0.06), 0 6px 16px -8px rgba(14, 14, 13, 0.14);
  transition: transform 180ms var(--ease-ui);
}

.carousel-arrows button:hover {
  transform: translateY(-1px) scale(1.02);
}

.carousel-arrows button:active { transform: scale(0.96); }
.carousel-arrows svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

/* The play triangle is a closed shape, so it needs the fill the arrows refuse.
   And `hidden` is an HTML attribute: the UA sheet applies it through a plain
   attribute selector, which an <svg> in an HTML document is not guaranteed to
   pick up the same way. Declaring it here means the toggle cannot end up
   showing both glyphs stacked on each other. */

.carousel-arrows button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.carousel-status {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.legal-bar {
  min-width: 0;
  /* Measured from the CARD, not the viewport: the header rides the card's inner
     edge and the two were 3-4px apart at both ends, at every width. 10px is the
     hero's own margin. */
  padding: 0 calc(11px + var(--frame-radius));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  color: var(--ink-3);
  font-size: 0.65rem;
  line-height: 1;
}

.legal-bar nav { display: flex; align-items: center; gap: clamp(13px, 1.5vw, 24px); }
.legal-bar a { transition: color 180ms var(--ease-ui); }
/* The links measured 10.9px tall — the text box and nothing else — so on a
   phone the whole legal row was a strip of sub-finger targets sitting 4px
   apart. The padding is vertical only and the bar is centred on its own line,
   so nothing above it moves: the target grows, the layout does not. */
.legal-bar nav a { display: inline-flex; align-items: center; min-height: 44px; }
.legal-bar a:hover { color: var(--ink); }
.legal-bar a:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.legal-bar .footer-sitenav { display: none; }

/* Mobile-LP sections (homepage only). Hidden on desktop, then shown below
   780px where the homepage becomes a top-to-bottom product and editorial flow. */
.lp-blog, .lp-faq, .lp-close { display: none; }

@media (min-width: 1025px) and (max-width: 1180px) {
  .hero__copy { left: 38px; width: 44vw; }
  h1 { font-size: clamp(2.85rem, 4.55vw, 4.45rem); }
  .product-showcase { left: 50%; right: 24px; }
  .feature-copy-stack, .carousel-controls { right: 4%; left: 4%; }
}

/* ===========================================================================
   THE MOBILE REGIME — everything at or below 1024px.

   Was 780px until 2026-08-30. The founder's call that day: the one-viewport
   lock is a DESKTOP signature, and a tablet served the desktop layout is a
   desktop compressed, not a tablet. Between 781 and 960 the site used to fall
   into a gap — a squeezed two-column hero with 300px of dead space above the
   copy and the carousel bleeding off the right edge. That gap is now part of
   this block, and the 781-960 patch that papered over it is gone.

   Above 1024px nothing changed: the card stays locked to one viewport.
   =========================================================================== */
@media (max-width: 1024px) {
  .site-frame { grid-template-rows: minmax(0, 1fr) var(--footer-bar); }
  .hero { margin: 7px 7px 0; border-radius: 26px; }
  .topbar { top: 14px; left: 14px; right: 14px; }
  .brand { width: 44px; height: 44px; flex-basis: 44px; }
  /* 44 in 44, for the same reason the desktop mark is 56 in 56: `place-items:
     center` falls back to start once the child overflows, so 52px of art in a
     44px box sent all 8px of overflow right and DOWN. Measured 2026-09-01 on
     every route below 1025 — the glove's centre at y=44 against y=40 for the
     burger and the bar. The art carries 3.5% of internal padding, so it still
     paints ~42px and stays the heaviest object in the bar. */
  .brand__mark { width: 44px; height: 44px; }
  .site-nav, .header-cta { display: none; }
  .mobile-nav { display: block; margin-left: auto; }

  .mobile-nav summary { width: 44px; min-height: 44px; }


  /* ---- Homepage (.mobile-scroll). The one-viewport lock is a desktop
     invariant; on mobile the frame releases its fixed height, every
     absolutely positioned region rejoins the document flow, and the page
     reads as one top-to-bottom scroll. The other pages keep the locked
     skeleton above, burger included, until their own mobile pass. */

  /* body's base `overflow: hidden auto` makes body itself a scroll container,
     and a sticky element sticks to its nearest scrollport -- body's, which
     never scrolls. clip guards the x axis without creating one. */
  .mobile-scroll { overflow: clip visible; }

  .mobile-scroll .site-frame { height: auto; display: block; }

  .mobile-scroll .hero {
    /* clip, not hidden: an overflow-hidden ancestor becomes the sticky
       topbar's scrollport and the bar would never stick. */
    overflow: clip;
  }

  .mobile-scroll .topbar {
    position: sticky;
    top: 0;
    left: auto;
    right: auto;
    padding: 10px 14px;
    /* 25px, not 26: the bar sits INSIDE the card's 1px border, so its radius is
       the card's outer radius minus that border. Same centre, R - d — the two
       arcs are concentric instead of merely both round. */
    border-radius: 25px 25px 0 0;
    background: rgba(245, 245, 243, 0.86);
    -webkit-backdrop-filter: blur(18px) saturate(115%);
    backdrop-filter: blur(18px) saturate(115%);
  }

  /* THE CARD'S TOP CORNERS, REDRAWN UNDER THE STUCK BAR (founder, 2026-08-30:
     "je veux que dans le header on ait toujours les arrondis dans les corners
     hauts, comme si on était toujours au début de la page").

     Rounding the bar alone did nothing visible: behind its corners sits the
     card, the same colour as the bar, so the arc had nothing to read against.
     The corner only exists if the PAGE shows through it — and the card's real
     rounded top scrolled out of the viewport long ago.

     So the bar paints them back. Each layer is the sliver of a 26x26 box lying
     OUTSIDE a 26px circle tangent to both of that box's inner edges: exactly
     the concave corner, white, over the card. -1px on each side so the card's
     own 1px border is covered too, and the arc reads as the border's own turn.

     It cannot touch the mark or the burger: the nearest pixel either of them
     owns is 20px from the circle centre and the fill only starts at 26px. */
  .mobile-scroll .topbar::before {
    content: "";
    position: absolute;
    z-index: 1;
    top: 0;
    left: -1px;
    right: -1px;
    height: 26px;
    pointer-events: none;
    background:
      radial-gradient(circle 26px at 100% 100%, transparent 0 25.4px, var(--paper) 26.4px) top left / 26px 26px no-repeat,
      radial-gradient(circle 26px at 0 100%, transparent 0 25.4px, var(--paper) 26.4px) top right / 26px 26px no-repeat;
  }

  /* The hairline that runs the card's edge, carried around the redrawn arc so
     the border does not appear to start 26px down the screen. Height stops at
     the arc's end — below it the card's own border takes over, no doubling. */
  .mobile-scroll .topbar::after {
    content: "";
    position: absolute;
    z-index: 1;
    top: 0;
    left: -1px;
    right: -1px;
    height: 26px;
    pointer-events: none;
    border: 1px solid rgba(14, 14, 13, 0.08);
    border-bottom: 0;
    border-radius: 26px 26px 0 0;
  }

  /* Pages that drop the burger (.no-mobile-menu) keep exactly two things in
     the header: the mark and a compact store pill. Navigation moves to the
     footer. Pages without the class (older HTML still on prod) keep the
     burger rules above. */
  .no-mobile-menu .site-frame { grid-template-rows: minmax(0, 1fr) auto; }

  /* NO STORE PILL IN THE MOBILE HEADER. Founder, 2026-08-30: *"je veux que tu
     retires le CTA et que tu laisses juste le menu"*. The bar carries the mark
     and the menu, nothing else; the download lives in the hero, where it is
     the page's own call to action, and again in the close. A sticky bar that
     repeats it a third time is chrome competing with content.
     `.site-nav, .header-cta { display: none }` above already does the hiding —
     what was here was the exception that put the pill back. */

  .hero__copy {
    position: static;
    width: auto;
    transform: none;
    padding: clamp(24px, 8vw, 42px) 21px 0;
  }

  .callout { gap: 8px; margin-bottom: 14px; font-size: clamp(0.58rem, 2.3vw, 0.68rem); white-space: normal; text-wrap: balance; }
  .callout span { flex: none; width: 18px; height: 2px; }
  h1 { max-width: none; font-size: clamp(1.65rem, 8.1vw, 3.2rem); line-height: 0.93; }
  .hero__sub { max-width: 44ch; margin: 14px 0 18px; font-size: clamp(0.78rem, 3.3vw, 0.9rem); line-height: 1.5; }
  .subheadline-line { display: inline; white-space: normal; }
  .subheadline-line + .subheadline-line::before { content: " "; }

  .store-actions { scroll-margin-top: 78px; }
  .store-cta { min-height: 50px; gap: 9px; padding: 0 17px 0 14px; }
  .store-cta svg { width: 18px; height: 22px; }
  .store-cta small { font-size: 0.52rem; }
  .store-cta strong { font-size: 0.92rem; }

  /* The app: the carousel comes back on mobile — one phone on stage, the
     copy synced above, swipe plus the two arrows below (founder, 2026-08-19).
     Same machinery as desktop, remapped into a fixed-height flow block. */
  .product-showcase {
    position: relative;
    top: auto;
    right: auto;
    bottom: auto;
    left: auto;
    height: 596px;
    margin-top: clamp(44px, 13vw, 68px);
  }

  .feature-copy-stack { top: 0; right: 4%; left: 4%; height: 88px; }
  .feature-copy p { margin-bottom: 6px; font-size: 0.56rem; }
  .feature-copy h2 { margin-bottom: 5px; font-size: clamp(1.2rem, 5vw, 1.5rem); }
  .feature-copy small { font-size: 0.66rem; line-height: 1.35; }

  .carousel-stage { inset: 96px 0 74px; }
  .iphone-mockup { height: min(92%, 400px); }
  .iphone-mockup.is-prev { transform: translate3d(-106%, -54%, -70px) scale(0.7) rotate(-3deg); }
  .iphone-mockup.is-next { transform: translate3d(6%, -54%, -70px) scale(0.7) rotate(3deg); }

  .carousel-controls { bottom: 8px; }
  .carousel-arrows button { width: 44px; height: 44px; }

  .lp-blog h2 {
    margin: 0 0 26px;
    color: var(--stage-text);
    font-family: var(--font-display);
    font-size: clamp(1.7rem, 7.2vw, 2.4rem);
    font-weight: 600;
    letter-spacing: -0.035em;
    line-height: 0.97;
  }

  /* ---- From the blog ----------------------------------------------------
     Rebuilt 2026-08-28 in the LEDGER'S grammar, on the founder's call — "tu
     mets à jour la LP avec les wireframes qu'on a créés pour la page Blog".
     Same three columns as /blog/: thumbnail, title + dek, date. A visitor who
     taps through must land on a page that looks like where they came from.

     MOBILE ONLY, and that is not a shortcut. The desktop landing page is one
     locked viewport — the site's signature, reaffirmed for /blog/ the same day
     ("on reste toujours sur cette idée d'une seule page"). There is no room
     below the card for a section, and giving the homepage a desktop scroll to
     make room would trade the invariant for three rows. On mobile the frame
     releases and the page already scrolls, so the section costs nothing.

     The rows are generated into index.html by tools/render.py, between the
     LPBLOG markers. With no posts the whole section is omitted. */
  .lp-blog {
    display: block;
    padding: clamp(60px, 16vw, 84px) 21px 0;
  }

  .lp-blog h2 { margin-bottom: 8px; }
  .lp-blog .eyebrow { margin: 0 0 14px; }

  .lp-blog__row {
    display: grid;
    grid-template-columns: 84px minmax(0, 1fr);
    grid-template-rows: auto auto;
    grid-template-areas: "thumb date" "thumb text";
    gap: 4px 16px;
    align-content: start;
    padding: 16px 0;
    border-bottom: 1px solid var(--stage-line);
  }
  .lp-blog__row:first-of-type { border-top: 1px solid var(--stage-line); }

  .lp-blog__thumb {
    grid-area: thumb;
    display: block;
    aspect-ratio: 16 / 10;
    overflow: hidden;
    border: 1px solid var(--stage-line);
    border-radius: 10px;
    background: var(--paper);
  }
  .lp-blog__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }

  .lp-blog__row time {
    grid-area: date;
    color: var(--ink-3);
    font-size: 0.74rem;
    font-weight: 560;
    font-variant-numeric: tabular-nums;
  }

  .lp-blog__row > span:last-of-type { grid-area: text; min-width: 0; }

  .lp-blog__row h3 {
    margin: 0 0 4px;
    color: var(--stage-text);
    font-size: 0.98rem;
    font-weight: 680;
    letter-spacing: -0.015em;
    line-height: 1.25;
  }

  .lp-blog__row p {
    margin: 0;
    color: var(--ink-3);
    font-size: 0.74rem;
    line-height: 1.45;
  }

  .lp-blog__all {
    display: inline-block;
    margin-top: 16px;
    color: var(--ink);
    font-size: 0.82rem;
    font-weight: 650;
  }

  /* ---- CENTRED, not left-ruled -----------------------------------------
     Founder, 2026-08-30: *"sur une version mobile, on pourrait creer une
     version beaucoup plus centree. C'est-a-dire que ce n'est pas aligne sur
     la marge de gauche, mais tout est centralise."*

     Everything that is a statement centres: the eyebrow, the headline, the
     sub, the store buttons, every section head, the close, the footer.
     Everything that is a LIST does not — a blog row is a thumbnail beside a
     title beside a dek, and a FAQ question is a line you scan down the left
     edge. Centring those costs the reading edge and buys nothing. */
  .hero__copy { text-align: center; }
  .callout { justify-content: center; }
  .hero__sub { margin-inline: auto; }
  .hero__copy .headline-line { margin-inline: auto; }
  .store-actions { justify-content: center; }

  .lp-blog, .lp-faq { text-align: center; }
  .lp-blog .eyebrow, .lp-faq .eyebrow { justify-content: center; }
  .lp-blog__row, .lp-faq__q { text-align: left; }
  /* `inline-block` gave them a 16px text box. They are the only way out of
     each mobile section, so they get a real target. */
  .lp-blog__all, .lp-faq__all { display: inline-flex; align-items: center; min-height: 44px; }

  .no-mobile-menu .footer-sitenav { justify-content: center; }
  .no-mobile-menu .legal-bar nav { justify-content: center; }

  /* ---- The FAQ teaser ---------------------------------------------------
     Same grammar as /faq/'s own accordion — a question, a hairline, a chevron
     that turns — so the four questions here and the twenty-two there read as
     one component. Generated by tools/render.py between the LPFAQ markers;
     the answers are the FAQ table's own, never a second copy. */
  .lp-faq {
    display: block;
    padding: clamp(56px, 15vw, 78px) 21px 0;
  }

  .lp-faq h2 {
    margin: 0 0 20px;
    color: var(--stage-text);
    font-family: var(--font-display);
    font-size: clamp(1.7rem, 7.2vw, 2.4rem);
    font-weight: 600;
    letter-spacing: -0.035em;
    line-height: 0.97;
  }

  .lp-faq .eyebrow { margin: 0 0 14px; }

  .lp-faq__q { border-bottom: 1px solid var(--stage-line); }
  .lp-faq__q:first-of-type { border-top: 1px solid var(--stage-line); }

  .lp-faq__q summary {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 16px 0;
    cursor: pointer;
    list-style: none;
    color: var(--stage-text);
    font-size: 0.95rem;
    font-weight: 640;
    letter-spacing: -0.017em;
    line-height: 1.3;
  }
  .lp-faq__q summary::-webkit-details-marker { display: none; }
  .lp-faq__q summary:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 4px; }

  /* A chevron, not a plus: the accordion on /faq/ uses one and the two must
     not disagree. Rotated borders rather than an SVG — one element, no asset. */
  .lp-faq__i {
    flex: none;
    margin-left: auto;
    width: 9px;
    height: 9px;
    border-right: 1.7px solid var(--ink-3);
    border-bottom: 1.7px solid var(--ink-3);
    transform: rotate(45deg) translate(-2px, -2px);
    transition: transform 220ms var(--ease-ui);
  }
  .lp-faq__q[open] .lp-faq__i { transform: rotate(-135deg) translate(-2px, -2px); }

  .lp-faq__q p {
    margin: -2px 0 16px;
    color: var(--ink-3);
    font-size: 0.86rem;
    line-height: 1.5;
  }

  .lp-faq__all {
    display: inline-block;
    margin-top: 16px;
    color: var(--ink);
    font-size: 0.82rem;
    font-weight: 650;
  }

  /* Final close — the promise again, then the store. */
  .lp-close {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: clamp(64px, 17vw, 92px) 21px clamp(64px, 16vw, 88px);
    text-align: center;
  }

  .lp-close h2 {
    margin: 0 0 24px;
    color: var(--stage-text);
    font-family: var(--font-display);
    font-size: clamp(1.6rem, 6.8vw, 2.2rem);
    font-weight: 600;
    letter-spacing: -0.035em;
    line-height: 0.97;
  }

  /* headline-line is max-content wide; centre each line inside the close. */
  .lp-close h2 .headline-line { margin-inline: auto; }

  .lp-close__soon {
    margin: 14px 0 0;
    color: var(--ink-3);
    font-size: 0.68rem;
  }

  /* "et Contact Us, ca reste tout en bas" — the last thing under the last CTA,
     for the reader who got to the end and still has a question. */
  .lp-close__contact {
    /* The last control on the mobile homepage, and it was 35px tall. The rule
       above it is `border-top`, so the growth has to go BELOW the text or the
       hairline moves. */
    display: inline-flex;
    align-items: flex-start;
    min-height: 44px;
    margin-top: 20px;
    padding-top: 18px;
    border-top: 1px solid var(--stage-line);
    color: var(--ink);
    font-size: 0.84rem;
    font-weight: 650;
    letter-spacing: -0.012em;
  }

  /* The footer joins the scroll: site links first, because the header
     deliberately carries no nav, then the legal row. */
  .no-mobile-menu .legal-bar {
    flex-direction: column;
    /* centred with the rest of the mobile page (founder, 2026-08-30) */
    align-items: center;
    text-align: center;
    gap: 16px;
    padding: 26px 21px calc(30px + env(safe-area-inset-bottom));
  }
  .no-mobile-menu .footer-sitenav { display: flex; gap: 24px; }
  .no-mobile-menu .footer-sitenav a {
    display: inline-flex;
    align-items: center;
    /* 12.8px of text box was the target. Same treatment as the legal row above
       it: vertical padding only, on a row that is already centred. */
    min-height: 44px;
    color: var(--ink-2);
    font-size: 0.8rem;
    font-weight: 650;
  }
  .no-mobile-menu .legal-bar > span { order: 3; }
  .no-mobile-menu .legal-bar nav { flex-wrap: wrap; }
}

/* ---------------------------------------------------------------------------
   TABLET (744-1024). Same regime as the phone — one column, document scroll —
   but a phone layout stretched to 1024px is its own defect: a 1000px measure
   and a phone lost in the middle of an empty stage. Everything here bounds
   the measure and gives the carousel back a stage worth its width. No new
   section, no element the desktop does not already have.
   ------------------------------------------------------------------------- */
@media (min-width: 744px) and (max-width: 1024px) {
  /* One measure for the whole page — 680px of content inside 34px of gutter.
     The topbar is full-bleed (it carries the blur) but its contents ride the
     same column, otherwise the mark sits alone at the far left while the
     headline starts 120px inboard. */
  /* `(100% - 680px) / 2` IS the column's left edge — `.pshell` is capped at 748
     and centred, so its 34px gutter is already inside that half-difference.
     Adding it again put the mark 34px right of the questions it sits over, at
     every width in the band. Measured 2026-09-01. */
  .mobile-scroll .topbar { padding-inline: max(14px, calc((100% - 680px) / 2)); }

  .hero__copy {
    max-width: 748px;
    margin-inline: auto;
    padding-top: clamp(40px, 6vw, 64px);
    padding-inline: 34px;
  }
  h1 { font-size: clamp(3rem, 5.6vw, 3.9rem); }
  .hero__sub { max-width: 52ch; margin-top: 18px; font-size: 0.95rem; }
  .callout { font-size: 0.7rem; }

  .store-cta { min-height: 56px; }
  .store-cta small { font-size: 0.56rem; }
  .store-cta strong { font-size: 1rem; }

  /* The stage scales with the viewport instead of staying at the phone's
     596px, and the flanking phones come back into frame. */
  .product-showcase { height: clamp(640px, 78vw, 860px); margin-top: clamp(56px, 8vw, 88px); }
  .feature-copy-stack { right: auto; left: 50%; width: min(560px, 86%); height: 104px; transform: translateX(-50%); }
  .feature-copy h2 { font-size: 1.75rem; }
  .feature-copy small { font-size: 0.8rem; }
  .carousel-stage { inset: 128px 0 84px; }
  .iphone-mockup { height: min(94%, 560px); }
  .iphone-mockup.is-prev { transform: translate3d(-92%, -54%, -70px) scale(0.74) rotate(-3deg); }
  .iphone-mockup.is-next { transform: translate3d(-8%, -54%, -70px) scale(0.74) rotate(3deg); }

  /* `.lp-faq` belongs here too and was missing until 2026-09-01: the homepage
     FAQ teaser ran full-bleed through the whole band while the blog rows above
     it rode the 748px column, so its questions started at x=29 against x=172
     for everything else at 1024 — 143px out, with hairlines running 1008px wide
     against a 680px measure. */
  .lp-blog, .lp-faq, .lp-close { max-width: 748px; margin-inline: auto; padding-inline: 34px; }
  .lp-blog h2 { font-size: 2.5rem; }
  .lp-blog__row { grid-template-columns: 128px minmax(0, 1fr); gap: 6px 22px; padding: 22px 0; }
  .lp-blog__row h3 { font-size: 1.16rem; }
  .lp-blog__row p { font-size: 0.86rem; }
  .lp-close h2 { font-size: 2.4rem; }

  .no-mobile-menu .legal-bar { max-width: 748px; margin-inline: auto; padding-inline: 34px; }
}

@media (max-width: 520px) {
  h1 { font-size: clamp(1.65rem, 8.1vw, 2.85rem); }
  .hero__copy { padding-inline: 17px; }
  .hero__sub { max-width: none; }
  .product-showcase { padding-inline: 14px; }
  .legal-bar { padding-inline: 14px; font-size: 0.58rem; }
  .legal-bar nav { gap: 10px; }
  /* `:not(.footer-sitenav)` because both navs are children of `.legal-bar`, so
     the bare selector also hit the SITE nav's third link — "Contact Us" — and
     dropped it into a display mode of its own. That is what raised it above
     "FAQ" and "Blog" once the links became 44px boxes.
     And `inline-flex`, not `inline`: the tap-target rule up at `.legal-bar nav
     a` makes every link a 44px centred box, and `display: inline` here was
     later in the cascade, so Safety and Cookies alone reverted to baseline
     alignment and sat a row above their neighbours. Two links out of six in a
     different vertical position reads as a broken footer, which is what it
     was. */
  .legal-bar nav:not(.footer-sitenav) a:nth-child(3),
  .legal-bar nav:not(.footer-sitenav) a:nth-child(4) { display: none; }
  /* The homepage footer stacks, so nothing needs to be sacrificed there. */
  .no-mobile-menu .legal-bar { padding-inline: 21px; font-size: 0.68rem; }
  .no-mobile-menu .legal-bar nav:not(.footer-sitenav) a:nth-child(3),
  .no-mobile-menu .legal-bar nav:not(.footer-sitenav) a:nth-child(4) { display: inline-flex; }
}

@media (max-width: 420px) {
  .store-actions { align-items: stretch; }
  .store-actions .store-cta { min-width: 0; flex: 1 1 0; padding-inline: 12px; }
}

/* Kept for the viewport-locked pages (About, FAQ): short portrait
   viewports still need the headline and CTAs reined in to fit. The homepage
   flows and no longer depends on this. */
/* A short PHONE in landscape: the hero has to give height back or the store
   pills fall under the fold. Keyed on 1024 like everything else since
   2026-09-01 — it was still on the retired 780 breakpoint, which left the whole
   744-1024 band without it. */
@media (max-height: 760px) and (max-width: 1024px) {
  h1 { font-size: clamp(1.65rem, 8.1vw, 2.6rem); }
  .store-cta { min-height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================================
   THE ROUTE SHELL — everything that is not the homepage.
   ---------------------------------------------------------------------------
   THE HEADER INVARIANT. Founder, 2026-08-28: "il y a une règle d'or à appliquer
   pour garder une cohérence visuelle, une constance : c'est que le header soit
   toujours le même, parce que quand je switch de page, il n'y a pas de
   changement à la taille des logos, la disposition, le CTA."

   Two mechanisms enforce it, and neither is a promise to be careful:

   1. The markup. `<header class="topbar">` is written ONCE, in index.html, and
      tools/render.py lifts it verbatim into every other route. There is no
      second copy to update. The only per-page difference is `aria-current`,
      and it paints exactly what the nav already paints on hover — the link
      darkens and its underline is revealed. It marks where you are; it changes
      nothing about the bar's size, layout or CTA.
   2. This stylesheet. `.topbar`, `.brand__mark`, `.site-nav` and
      `.header-cta` are declared here and NOWHERE else. A route stylesheet
      (contact.css, faq.css, blog.css, legal.css) owns its own body and is
      forbidden to touch the chrome — no topbar override, no background
      override, no radius of its own on `.hero`.

   The same argument applies one level down, to the headline. Skiv arrived at
   this the hard way and wrote it into contact.css: "pour creer cette coherence
   visuel ca vient surtout de la headline, sa taille et son emplacement qui doit
   rester constant" — but it got there by COPYING the same six declarations into
   four route stylesheets, which is the drift problem again with a longer fuse.
   Here the shell and the headline are declared once, below, and the routes
   inherit them.
   ========================================================================== */

body.route {
  /* Both shared with the homepage grid. Do not round either to a nicer
     number: --gutter IS the topbar's own left/right inset, which is what lines
     the headline up with the mark above it.
     It is `var(--frame-radius)` and not a clamp of its own because that is what
     `.topbar` takes (top/left/right) since 2026-09-01. While the two curves
     were written separately they disagreed by 2.2-2.9px at every width between
     1025 and ~1650 — the mast sat at x=47 under a mark at x=44.11 at 1440 —
     and the legal bar, aligned to the card inset, made it a three-way stagger.
     One token, one edge. */
  --gutter: var(--frame-radius);
  --shell: 1660px;
  /* A notch darker than --line (0.1): a field is a control, a divider is not. */
  --field-line: rgba(14, 14, 13, 0.2);
}

/* The .hero has to be a flex column or a route wrap's `flex: 1 1 auto` is inert
   and the wrap takes its CONTENT height — silently clipped by
   `.hero { overflow: hidden }`. The clip hides the cause and shows only the
   symptom, so this is found by measuring, never by looking. */
body.route .hero { display: flex; flex-direction: column; min-height: 0; }

.pshell {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  /* Clears the absolutely positioned topbar. Dropped at 1024, where the bar
     rejoins the flow as a sticky element and this padding becomes a hole. */
  padding: clamp(84px, 8.4vw, 118px) var(--gutter) 0;
}

.pmast {
  flex: none;
  margin: 0;
  padding-bottom: clamp(12px, 1.4vw, 20px);
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(1.85rem, 2.9vw, 2.6rem);
  font-weight: 680;
  letter-spacing: -0.042em;
  line-height: 1;
}

/* Nothing sits above the headline. A route that wants to say what it is says it
   in the headline — an eyebrow pill reading "Contact" directly over a headline
   reading "Contact Us" carries nothing and was deleted on sight on skiv.app. */

/* A small label above a rail or a panel. Sentence-case sans at eyebrow weight —
   never the mono UPPERCASE micro-label. Shared, because it was declared
   identically in faq.css and legal.css, which is one copy too many. */
.eyebrow {
  margin: 0;
  color: var(--ink-3);
  font-size: 0.74rem;
  font-weight: 620;
  letter-spacing: -0.005em;
  line-height: 1;
}

/* A page-level scroll region: the route body scrolls inside the locked card
   rather than the card growing. `scrollbar-width: thin` swaps macOS's overlay
   bar for Chromium's classic one, which paints an opaque near-white track — a
   white band down the right edge of a #F5F5F3 card. The SECOND value of
   `scrollbar-color` is the track; transparent hands it back to the ground.
   Never pair this with `::-webkit-scrollbar` rules: declaring them makes
   Chromium ignore `scrollbar-color` outright. */
.pscroll {
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(14, 14, 13, 0.22) transparent;
  scrollbar-gutter: stable;
}

/* The skip link. Off-screen until focused, then it lands on the card in the
   chrome's own register — it is the first thing a keyboard user meets, so it
   cannot look like a bug. Never `display: none`: that removes it from the tab
   order and the link stops existing for the only people who need it. */
.skiplink {
  position: absolute;
  z-index: 100;
  top: 0;
  left: 0;
  padding: 12px 18px;
  transform: translateY(-140%);
  border-radius: 0 0 12px 0;
  background: var(--ink);
  /* `var(--card)` was never declared anywhere in assets/css — the link resolved
     to --ink on --ink, contrast 1.000:1, on every route since it was written.
     It is the first focusable element on the site. */
  color: var(--paper);
  font-size: 0.82rem;
  font-weight: 560;
  letter-spacing: -0.01em;
  text-decoration: none;
}

.skiplink:focus-visible {
  transform: translateY(0);
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}

/* Below 1025 the mark starts at (22,18), so a skip link at (0,0) covers the top
   half of the glove when focused — it looks exactly like the corner bug it is
   not. Dropped clear of the bar instead. Above 1025 it deliberately overlaps
   the card corner and stays as it is.

   IT HAS TO LIVE HERE, after the base rule, not up in the ≤1024 block with its
   siblings. Both selectors are `.skiplink` — identical specificity — and a
   media query adds none, so the later declaration wins regardless of the query.
   Written in the mobile block first, it was dead CSS: measured at 390 the
   focused link still landed at (0,0). Same trap as `.pshell` below. */
@media (max-width: 1024px) {
  .skiplink {
    top: 8px;
    left: 62px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    border-radius: 12px;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .skiplink { transition: transform 0.16s var(--ease-ui); }
}

/* A short desktop window gives the mast less room to clear the absolute bar.
   This lives in main.css and applies to EVERY route, because `.pshell` is the
   shell: contact.css carried its own copy until 2026-08-30 and that made one
   page's headline sit 20px higher than its neighbours'. */
@media (min-width: 1025px) and (max-height: 830px) {
  .pshell { padding-top: 112px; }
}

/* ---------------------------------------------------------------- mobile --- */

/* The routes release the one-viewport lock at 1024, WITH the homepage.

   They used to release at 900 while the homepage released at 780, and the
   comment here described that split as deliberate. The 2026-08-30 pass moved
   the homepage to 1024 and did not move the routes, which left 781-1024 a
   hybrid: the topbar was already sticky and in the flow (every page carries
   `mobile-scroll`, so the sticky rule in the 1024 block applies to routes
   too), while the route body still cleared an absolute bar, still rode
   `--gutter` instead of the chrome's 680px column, and still sized its frame
   with grid. Measured at 950px before the fix: an 84px hole under the bar, the
   brand at x=169 against a headline at x=32, and 260px of dead white under
   /blog/'s footer — the same white the flex rule below was written to kill,
   alive again one breakpoint up. One regime, one number. */
@media (max-width: 1024px) {
  body.route { overflow: clip visible; }

  /* Flex, not grid, once the lock is released. With `height: auto` the grid
     sizes its `1fr` track against the CONTENT height and `min-height: 100svh`
     then stretches the container without redistributing to the track — so a
     short route (/blog/ with two posts) painted its card at 480px inside an
     844px frame and left 196px of dead white under the footer (measured
     2026-08-30). In a column flexbox the card simply takes the slack. */
  body.route .site-frame { display: flex; flex-direction: column; height: auto; min-height: 100svh; }
  body.route .hero { flex: 1 0 auto; overflow: visible; }
  body.route .legal-bar { flex: none; }

  /* The bar is sticky and in the flow from here down, so the padding that
     cleared it above becomes a hole. Dropped in the same block that makes it
     sticky — the two must never disagree again. */
  .pshell { padding: clamp(14px, 3.5vw, 22px) 21px 0; }
  .pscroll { overflow: visible; scrollbar-gutter: auto; }
}

/* The route body rides the same 680px column as the topbar and the legal bar.
   Without it the body keeps the phone's 21px gutter while the chrome is
   already inboard, and the brand sits 140px to the right of the headline it
   sits above (measured at 950px).

   It has to live HERE, below `.pshell`'s base rule, and not in the tablet
   block up at line 1139 where it belongs by subject: a media query adds no
   specificity, so `.pshell { max-width: var(--shell) }` declared later in the
   file beats an identical selector declared earlier inside `@media`. That is
   the cost of main.css keeping its shell base rules after its media queries —
   the ordering is worth fixing, but not in the same pass as a live layout. */
@media (min-width: 744px) and (max-width: 1024px) {
  .pshell { max-width: 748px; padding-inline: 34px; }
}

/* ---------------------------------------------------- the one exception --- */

/* THE ARTICLE ROUTE RELEASES THE LOCK AT EVERY WIDTH. Founder, 2026-08-28:
   *"quand je clique sur l'article, ça ouvre une nouvelle page, et là il n'y a
   plus cette idée d'une seule page où on pourra scroll."* The hub keeps the
   internal scroll; the article is a document and the window scrolls it. The
   card grows with the read and the legal bar lands after it.

   It lives HERE, not in article.css, because it is the frame — and the frame,
   like the topbar, is main.css's to own. A route stylesheet owns its body. */
.doc-scroll { overflow: clip visible; }
/* `auto` collapsed the track to its tallest child — 26px of social icons — so
   the bar was 42px on the locked routes and 26px on the scrolling ones, and the
   icon row touched the bottom of the document with no air under it. */
.doc-scroll .site-frame { height: auto; min-height: 100svh; grid-template-rows: minmax(0, 1fr) var(--footer-bar); }
.doc-scroll .hero { overflow: visible; }

/* The topbar stays absolute and scrolls away with the document. It is NOT made
   sticky: the sticky treatment above adds a translucent blurred background, and
   a header that grows a background on one route is a header that changed. */
