/* =========================================================================
   TRIBOS - Site design system « Noir & Blaze »
   Fixed on 2026-10-14. The rules, the component catalogue and the pre-flight
   checklist live in .claude/skills/tribos-site-design-system/SKILL.md: read
   it before any visual change. This file is its implementation; a new token
   or component is added in both places, never in one only.

   Three layers, never merged:
     1. the warm black of the page (--bg) and its graphite surfaces;
     2. orange: one accent (--accent) and one poster surface, .blaze, kept for
        a few moments per page (the old site's signature, on purpose);
     3. the Cinematic 3D artwork (assets/cine/): the canonical fox at sunset,
        shipped as images that never carry text.

   Radius rule:
     - interactive (buttons, inputs, chips)  -> full pill
     - cards, panels, tiles                   -> var(--r-card)  18px
     - media, phone frames, large panels      -> var(--r-media) 26px

   Contrast notes (WCAG AA, measured):
     - White on #F3730A is 2.9:1 -> banned. Orange fills carry --on-accent.
     - Orange as small text uses --accent-text (#FFA25C, 9.9:1 on --bg).
     - On .blaze, copy sits only on the deep zones (<= #C2410C, white 5:1);
       the hot corner takes the ivory button, never a line of text.
     - Text over artwork sits on a measured veil (.scene).
   ========================================================================= */

/* ---------- Tokens ---------- */
:root {
  color-scheme: dark;

  /* One family, three jobs. The artwork carries the personality; the type
     stays neutral, bold and tight. */
  --font-display: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-text: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --step--1: clamp(0.82rem, 0.8rem + 0.1vw, 0.875rem);
  --step-0: clamp(1rem, 0.97rem + 0.16vw, 1.0625rem);
  --step-1: clamp(1.13rem, 1.06rem + 0.34vw, 1.3125rem);
  --step-2: clamp(1.35rem, 1.2rem + 0.72vw, 1.75rem);
  --step-3: clamp(1.75rem, 1.38rem + 1.7vw, 2.85rem);
  --step-4: clamp(2.2rem, 1.5rem + 3.1vw, 4.1rem);
  --step-5: clamp(2.6rem, 1.55rem + 4.7vw, 5.6rem);

  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;
  --s-8: 4rem;
  --s-9: 6rem;
  --s-10: 8rem;

  --r-card: 18px;
  --r-media: 26px;
  --r-pill: 999px;

  --wrap: 1280px;
  --wrap-narrow: 780px;
  --gutter: clamp(1.25rem, 0.8rem + 2vw, 3rem);
  --nav-h: 72px;

  /* Motion. --motion is a distance multiplier: reduced motion sets it to 0, so
     every authored translate collapses while opacity cross-fades still play. */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-film: cubic-bezier(0.22, 0.61, 0.36, 1);
  --motion: 1;
  --lift: calc(var(--motion) * 1px);

  /* Warm black: the old site's canvas, kept on purpose. */
  --bg: #0d0b09;
  --bg-2: #131110;
  --surface: #1a1613;
  --surface-2: #221d19;
  --line: rgb(255 236 214 / 0.08);
  --line-strong: rgb(255 236 214 / 0.16);

  /* Warm ivory text, never pure white. */
  --text: #f6f1ea;
  --text-body: #beb5aa;
  --text-muted: #a09385;

  /* Orange. --accent is AppTheme.primary and fills; it never carries white. */
  --accent: #f3730a;
  --accent-hi: #ff8a2a;
  --accent-text: #ffa25c;
  --on-accent: #221000;
  --copper: #d94f16;
  --cream: #fdf4ea;
  --petrol: #237f7a;

  /* Blaze: the orange poster surface (see .blaze). */
  --blaze-hi: #f58022;
  --blaze-mid: #c2410c;
  --blaze-deep: #9a3607;
  --blaze-lo: #571c02;
  --on-blaze: #ffffff;
  --on-blaze-dim: rgb(255 255 255 / 0.92);
  --blaze-line: rgb(255 255 255 / 0.28);

  /* Glass: dark translucent panels, used only over artwork or glow. */
  --glass: rgb(26 22 19 / 0.72);
  --glass-line: rgb(255 236 214 / 0.12);

  /* Forms default to the dark panel treatment; .blaze remaps these. */
  --panel-fg: var(--text);
  --panel-fg-dim: var(--text-body);
  --panel-line: rgb(255 236 214 / 0.24);
  --panel-line-hover: rgb(255 236 214 / 0.42);
  --panel-input: rgb(8 6 5 / 0.55);
  --panel-placeholder: #9a8f84;
  --panel-help: #b0a69b;
  --panel-link: var(--accent-text);

  --shadow-1: 0 1px 2px rgb(0 0 0 / 0.3), 0 6px 18px rgb(0 0 0 / 0.28);
  --shadow-2: 0 2px 8px rgb(0 0 0 / 0.34), 0 22px 48px rgb(0 0 0 / 0.42);
  --shadow-3: 0 6px 14px rgb(0 0 0 / 0.38), 0 44px 100px rgb(0 0 0 / 0.55);
  --glow: 0 10px 34px rgb(243 115 10 / 0.32);
  --grain: 0.05;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 1.5rem);
  /* `clip`, not `hidden`: a hidden root becomes a scroll container with
     nothing to scroll, which freezes every scroll-driven timeline on frame 0. */
  overflow-x: clip;
}

body {
  position: relative;
  margin: 0;
  background: var(--bg);
  color: var(--text-body);
  font-family: var(--font-text);
  font-size: var(--step-0);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Decorative light (the rim-light pools, the horizon line) is allowed to
   spill past its box, never past the page: on a phone a 56px spill was enough
   to widen the layout viewport and push the fixed dock off screen. `clip`
   is not a scroll container, so the pinned phone stays sticky. */
main {
  overflow-x: clip;
}

/* The last light of the day, behind the top of every page. Pages that open on
   a cinematic hero cover it; the text pages keep a warm horizon above them. */
body::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  z-index: -1;
  height: min(90vh, 900px);
  background:
    radial-gradient(55% 70% at 82% -12%, rgb(243 115 10 / 0.2), transparent 70%),
    radial-gradient(40% 50% at 8% -20%, rgb(217 79 22 / 0.12), transparent 70%);
  pointer-events: none;
}

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

/* Every <img> carries width/height to reserve space; `height: auto` keeps the
   attribute from stretching the image once CSS caps its width. */
img {
  height: auto;
}

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

h1,
h2,
h3,
h4 {
  margin: 0;
  color: var(--text);
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1.04;
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

a {
  color: inherit;
  text-decoration: none;
}

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

:focus-visible {
  outline: 3px solid var(--accent-text);
  outline-offset: 3px;
  border-radius: 6px;
}

/* On orange surfaces the amber ring disappears: it flips to ink. */
.tile-accent :focus-visible,
.btn-primary:focus-visible {
  outline-color: var(--text);
}

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

/* ---------- Utilities ---------- */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

.section {
  position: relative;
  padding-block: var(--section-air, clamp(4.5rem, 2.5rem + 7vw, 9.5rem));
}

.section-tight {
  position: relative;
  padding-block: clamp(3rem, 2rem + 4vw, 5.5rem);
}

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

/* Tabular figures stop numbers shifting width while a counter ticks. */
.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums lining;
  letter-spacing: -0.02em;
}

.skip-link {
  position: absolute;
  left: 50%;
  top: 0;
  translate: -50% -200%;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--text);
  color: var(--bg);
  border-radius: 0 0 var(--r-card) var(--r-card);
  font-weight: 600;
}

.skip-link:focus-visible {
  translate: -50% 0;
}

/* Film grain, fixed. Below the nav (50) on purpose: a blended layer over a
   backdrop-filter forces the whole page into one blending group. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: screen;
}

/* ---------- Type helpers ---------- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 var(--s-4);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-text);
}

.h1 {
  font-size: var(--step-4);
  letter-spacing: -0.04em;
}

.h2 {
  font-size: var(--step-4);
  letter-spacing: -0.04em;
  line-height: 1.02;
}

.h3 {
  font-size: var(--step-2);
  letter-spacing: -0.02em;
}

.lede {
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-body);
  max-width: 46ch;
}

/* Orange emphasis inside a heading: lit, and leaning forward. A skew, not a
   synthesised italic: Geist has no italic cut and the browser's fake oblique
   is heavier on the eye than a measured lean. */
.lit {
  display: inline-block;
  color: var(--accent-hi);
  font-style: normal;
  transform: skewX(-8deg);
  transform-origin: 0 100%;
  padding-right: 0.06em;
}

/* ---------- Buttons ---------- */
.btn {
  --btn-bg: var(--accent);
  --btn-fg: var(--on-accent);
  --btn-bd: transparent;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  min-height: 52px;
  padding: 0 1.6rem;
  border: 1px solid var(--btn-bd);
  border-radius: var(--r-pill);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-text);
  font-size: var(--step-0);
  font-weight: 600;
  letter-spacing: -0.005em;
  white-space: nowrap;
  cursor: pointer;
  transition: translate 0.2s var(--ease), box-shadow 0.25s var(--ease),
    background-color 0.2s var(--ease), border-color 0.2s var(--ease), color 0.2s var(--ease);
}

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

.btn:active {
  translate: 0 0;
  scale: 0.985;
  box-shadow: var(--shadow-1);
}

.btn-primary {
  box-shadow: var(--shadow-1);
}

.btn-primary:hover {
  --btn-bg: #ff8126;
  box-shadow: var(--glow), var(--shadow-1);
}

/* Ivory pill: the nav CTA, readable over sky and black alike. */
.btn-light {
  --btn-bg: var(--text);
  --btn-fg: var(--on-accent);
}

.btn-light:hover {
  --btn-bg: #ffe6cc;
}

.btn-ghost {
  --btn-bg: rgb(255 236 214 / 0.04);
  --btn-fg: var(--text);
  --btn-bd: var(--line-strong);
}

.btn-ghost:hover {
  --btn-bd: rgb(255 236 214 / 0.5);
  --btn-bg: rgb(255 236 214 / 0.08);
}

.btn-sm {
  min-height: 44px;
  padding: 0 1.15rem;
  font-size: var(--step--1);
}

.btn-lg {
  min-height: 58px;
  padding: 0 2rem;
  font-size: var(--step-1);
}

.btn .icon {
  width: 1.1em;
  height: 1.1em;
  flex: none;
}

.btn[aria-busy="true"] {
  pointer-events: none;
  opacity: 0.75;
}

.link-arrow {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  gap: 0.45rem;
  color: var(--text);
  font-weight: 600;
  text-underline-offset: 0.3em;
  text-decoration-color: var(--accent);
}

.link-arrow:hover {
  text-decoration: underline;
  text-decoration-thickness: 2px;
}

.link-arrow .icon {
  width: 1.05em;
  height: 1.05em;
  color: var(--accent-text);
  transition: translate 0.25s var(--ease);
}

.link-arrow:hover .icon {
  translate: 3px 0;
}

/* ---------- Nav ---------- */
/* Transparent over the opening shot, black glass once the page moves. */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  border-bottom: 1px solid transparent;
  transition: background-color 0.35s var(--ease), border-color 0.35s var(--ease),
    -webkit-backdrop-filter 0.35s var(--ease), backdrop-filter 0.35s var(--ease);
}

.nav[data-stuck="true"] {
  background: rgb(13 11 9 / 0.74);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  backdrop-filter: blur(16px) saturate(140%);
  border-bottom-color: var(--line);
}

@media (prefers-reduced-transparency: reduce) {
  .nav[data-stuck="true"] {
    background: var(--bg);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  width: 100%;
}

.brand {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  gap: 0.65rem;
  font-family: var(--font-display);
  font-size: 1.32rem;
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--text);
}

/* The app icon (3D fox on orange, from the app repo): 32 CSS px, so the 64 px
   export stays sharp at 2x. */
.brand img {
  width: 32px;
  height: 32px;
  border-radius: 9px;
  box-shadow: 0 4px 14px rgb(243 115 10 / 0.3);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 0.4rem + 2.4vw, 3rem);
  font-size: var(--step--1);
  font-weight: 500;
  color: rgb(246 241 234 / 0.84);
}

.nav-links a {
  position: relative;
  padding-block: 0.4rem;
  white-space: nowrap;
  transition: color 0.2s var(--ease);
}

.nav-links a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  scale: 0 1;
  transform-origin: left;
  transition: scale 0.28s var(--ease);
}

.nav-links a:hover {
  color: var(--text);
}

.nav-links a:hover::after,
.nav-links a[aria-current="page"]::after {
  scale: 1 1;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Reading-progress rail under the stuck nav. CSS scroll timeline, no listener. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll(root block)) {
    .nav::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: -1px;
      height: 2px;
      background: linear-gradient(90deg, var(--copper), var(--accent), var(--accent-text));
      transform-origin: 0 50%;
      scale: 0 1;
      opacity: 0;
      transition: opacity 0.35s var(--ease);
      animation: railGrow linear both;
      animation-timeline: scroll(root block);
    }

    .nav[data-stuck="true"]::after {
      opacity: 1;
    }

    @keyframes railGrow {
      to {
        scale: 1 1;
      }
    }
  }
}

.menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: rgb(13 11 9 / 0.4);
  color: var(--text);
  cursor: pointer;
}

.menu-toggle .icon {
  width: 20px;
  height: 20px;
}

.menu-toggle .icon-close {
  display: none;
}

.menu-toggle[aria-expanded="true"] .icon-open {
  display: none;
}

.menu-toggle[aria-expanded="true"] .icon-close {
  display: block;
}

/* A class rule beats the UA `[hidden] { display: none }`: opt back out. */
.menu-panel[hidden] {
  display: none;
}

.menu-panel {
  position: fixed;
  inset: var(--nav-h) 0 0;
  z-index: 45;
  display: grid;
  align-content: start;
  gap: var(--s-2);
  padding: var(--s-6) var(--gutter) var(--s-8);
  background:
    radial-gradient(80% 50% at 100% 100%, rgb(243 115 10 / 0.18), transparent 70%),
    var(--bg);
  border-top: 1px solid var(--line);
  overflow-y: auto;
}

.menu-panel a {
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* The menu's link colour must not reach the button: ivory on orange is the
   one pairing this site bans. */
.menu-panel .btn {
  margin-top: var(--s-5);
  border-bottom: 0;
  color: var(--btn-fg);
  font-family: var(--font-text);
  font-size: var(--step-0);
}

/* While the menu is open the nav loses its transparency, or the panel's top
   edge would show the hero through a glass-less bar. */
body:has(.menu-panel:not([hidden])) .nav {
  background: var(--bg);
  border-bottom-color: var(--line);
}

@media (max-width: 900px) {
  .nav-links {
    display: none;
  }
  .menu-toggle {
    display: inline-flex;
  }
}

@media (min-width: 901px) {
  .menu-panel {
    display: none;
  }
}

/* At 320px the wordmark collides with the CTA: drop the word, keep the mark
   and the link's aria-label. */
@media (max-width: 379px) {
  .nav .brand-name {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* =========================================================================
   Cinematic scenes

   .scene is the shared frame for every artwork that carries interface: the
   home hero, the closing call to action, and the heads of the secondary
   pages. One image, three veils:
     ::before  the legibility veil (left and bottom, measured for AA);
     ::after   the edges, which hand the image over to the black of the page.
   ========================================================================= */
.scene {
  position: relative;
  isolation: isolate;
  overflow: clip;
  background: var(--bg);
}

.scene-media {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: clip;
}

.scene-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focus, 62% 42%);
}

.scene::before,
.scene::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

/* The left veil is shaped around the text column, not a flat wash: the art
   keeps its full colour on the fox and the sun-lit right half. */
.scene::before {
  background:
    radial-gradient(42% 38% at 13% 21%, rgb(13 11 9 / 0.58), transparent 72%),
    radial-gradient(50% 42% at 16% 80%, rgb(13 11 9 / 0.55), transparent 72%),
    linear-gradient(
      90deg,
      rgb(13 11 9 / 0.9) 0%,
      rgb(13 11 9 / 0.8) 22%,
      rgb(13 11 9 / 0.54) 38%,
      rgb(13 11 9 / 0.14) 54%,
      transparent 66%
    );
}

.scene::after {
  background:
    linear-gradient(180deg, rgb(13 11 9 / 0.62) 0%, transparent 16%),
    linear-gradient(0deg, var(--bg) 0%, rgb(13 11 9 / 0.72) 9%, transparent 26%);
}

/* Opening shot: the image pushes in from slightly wide and slightly dark, the
   way a film opens on a landscape. Gated on .js so a failed script never
   leaves the hero dimmed; site.js adds .is-lit once the image has decoded. */
@media (prefers-reduced-motion: no-preference) {
  .js .scene-media[data-pushin] img {
    scale: 1.07;
    filter: brightness(0.62) saturate(0.88);
    transition: scale 2.8s var(--ease-film), filter 1.8s var(--ease-film);
  }

  .js .scene-media[data-pushin].is-lit img {
    scale: 1;
    filter: none;
  }
}

/* Decorative parallax: the landscape drifts slower than the page. Purely a
   CSS scroll timeline; unsupported browsers keep a still image. */
@media (prefers-reduced-motion: no-preference) and (min-width: 760px) and (orientation: landscape),
  (prefers-reduced-motion: no-preference) and (min-width: 1200px) {
  @supports (animation-timeline: view()) {
    /* Opening shot: drifts down as it leaves, so the page slides over it.
       The inset is pinned to 0: by default view() insets the range by the
       root's scroll-padding (the nav allowance), which started the drift
       96px in and opened a gap above the image on load. */
    .scene-media[data-drift="exit"] {
      animation: sceneExit linear both;
      animation-timeline: view(block 0px);
      animation-range: exit 0% exit 100%;
    }

    /* Mid-page scene: taller than its frame, it travels through it. */
    .scene-media[data-drift="through"] {
      inset: -9% 0;
      animation: sceneThrough linear both;
      animation-timeline: view(block 0px);
      animation-range: cover 0% cover 100%;
    }

    @keyframes sceneExit {
      to {
        translate: 0 18%;
      }
    }

    @keyframes sceneThrough {
      from {
        translate: 0 -7%;
      }
      to {
        translate: 0 7%;
      }
    }
  }
}

/* ---------- Home hero ---------- */
.hero {
  min-height: max(640px, min(100svh, 1020px));
  margin-top: calc(-1 * var(--nav-h));
  padding-top: calc(var(--nav-h) + clamp(1.5rem, 5vh, 4rem));
  padding-bottom: clamp(5rem, 13vh, 9rem);
  display: grid;
  align-items: center;
}

.hero .scene-media {
  --focus: 64% 38%;
}

.hero-inner {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
}

/* The shot scales with the viewport width, so on a large screen a centred
   1280px column would slide the copy onto the fox. The nav and the hero take
   a proportional width there instead, keeping the copy in the shadowed third
   of the image at every size. */
.nav .wrap,
.hero .wrap {
  max-width: max(var(--wrap), 86vw);
}

.hero-copy {
  max-width: 40rem;
}

.hero-copy .eyebrow {
  color: var(--text);
  letter-spacing: 0.12em;
  margin-bottom: var(--s-5);
}

.hero-copy h1 {
  font-size: min(clamp(2.9rem, 1rem + 5.4vw, 6rem), 12.5vh);
  font-weight: 700;
  letter-spacing: -0.047em;
  line-height: 0.95;
  margin-bottom: var(--s-6);
}

.hero-copy h1 .l {
  display: block;
}

.hero-copy .lede {
  max-width: 33ch;
  color: rgb(246 241 234 / 0.86);
  font-size: var(--step-1);
}

.hero-form {
  margin-top: var(--s-6);
  max-width: 34rem;
}

/* The help line is the smallest text laid on the art: brighter than the
   panel help, and, where the art sits behind it, short enough to stay off
   the fox's tail. */
.hero .help {
  color: rgb(246 241 234 / 0.8);
}

@media (min-width: 760px) {
  .hero .help {
    max-width: min(27rem, 30vw);
  }
}

.hero-secondary {
  margin-top: var(--s-4);
  font-size: var(--step--1);
}

/* Chapter index on the right edge: the three themes of the page, each a jump
   to its section. Small, calm, and only where the art leaves room for it. */
.hero-chapters {
  display: none;
}

/* Anchored low, over the dark pines at the foot of the shot: higher up they
   sat on the lit sky and fell under AA. */
@media (min-width: 1180px) {
  /* The containing block is the wrap's padding box: offset by the gutter so
     the index lines up with the nav CTA above it. */
  .hero-chapters {
    position: absolute;
    right: var(--gutter);
    bottom: 0;
    display: grid;
    gap: 1.4rem;
  }

  .hero-chapters a {
    display: grid;
    grid-template-columns: 34px auto;
    align-items: center;
    gap: 0.9rem;
    color: rgb(246 241 234 / 0.92);
    font-size: var(--step-0);
    font-weight: 500;
    text-shadow: 0 1px 14px rgb(6 4 3 / 0.7);
    transition: color 0.2s var(--ease), translate 0.3s var(--ease);
  }

  .hero-chapters .icon {
    width: 30px;
    height: 30px;
    filter: drop-shadow(0 1px 10px rgb(6 4 3 / 0.6));
  }

  .hero-chapters a:hover {
    color: var(--accent-text);
    translate: -4px 0;
  }

  /* Same veil as every scene, plus a soft shadow pool under the chapter
     index: on wide screens it lands on sun-lit pine needles. */
  .hero::before {
    background:
      radial-gradient(22% 30% at 91% 74%, rgb(13 11 9 / 0.62), transparent 72%),
      radial-gradient(42% 38% at 13% 21%, rgb(13 11 9 / 0.58), transparent 72%),
      radial-gradient(50% 42% at 16% 80%, rgb(13 11 9 / 0.55), transparent 72%),
      linear-gradient(
        90deg,
        rgb(13 11 9 / 0.9) 0%,
        rgb(13 11 9 / 0.8) 22%,
        rgb(13 11 9 / 0.54) 38%,
        rgb(13 11 9 / 0.14) 54%,
        transparent 66%
      );
  }
}

/* Short laptop screens (1280x720, 1366x768): the whole conversion block,
   help line included, has to land above the fold. */
@media (min-width: 760px) and (max-height: 820px) {
  .hero {
    padding-top: calc(var(--nav-h) + 0.75rem);
    padding-bottom: 3.25rem;
  }

  .hero-copy .eyebrow {
    margin-bottom: var(--s-4);
  }

  .hero-copy h1 {
    font-size: min(clamp(2.9rem, 1rem + 5.4vw, 6rem), 11.2vh);
    margin-bottom: var(--s-5);
  }

  .hero-form {
    margin-top: var(--s-5);
  }

  .hero-secondary {
    margin-top: var(--s-2);
  }
}

/* Phones and portrait tablets: every scene stacks. A screen taller than wide
   cannot hold the copy beside the fox (at 820x1180 the headline sat on its
   face), so the shot comes first, cropped square-ish on the fox (the portrait
   sources), and the copy rises over the foot of the image where it has
   already turned to black. The fades move onto the image box, so they follow
   its height instead of the section's. */
@media (max-width: 759px), (orientation: portrait) and (max-width: 1199px) {
  .scene.is-stacked {
    min-height: 0;
    display: block;
    padding-top: 0;
    padding-bottom: var(--s-7);
  }

  .scene.is-stacked::before,
  .scene.is-stacked::after {
    display: none;
  }

  .scene.is-stacked .scene-media {
    position: relative;
    inset: auto;
    aspect-ratio: 15 / 16;
    --focus: 50% 30%;
  }

  .scene.is-stacked .scene-media::after {
    content: "";
    position: absolute;
    inset: 0;
    background:
      linear-gradient(180deg, rgb(13 11 9 / 0.7) 0%, transparent 16%),
      linear-gradient(0deg, var(--bg) 0%, rgb(13 11 9 / 0.9) 20%, rgb(13 11 9 / 0.4) 38%, transparent 52%);
    pointer-events: none;
  }

  .scene.is-stacked .scene-body {
    position: relative;
    margin-top: calc(-0.24 * 100vw);
  }

  .scene-band.is-stacked {
    height: auto;
    padding-bottom: 0;
  }

  .scene-band.is-stacked .scene-media {
    aspect-ratio: 15 / 14;
    --focus: 50% 12%;
  }

  .scene-band.is-stacked:not(.is-top) .scene-media::after {
    background:
      linear-gradient(180deg, var(--bg) 0%, transparent 18%),
      linear-gradient(0deg, var(--bg) 0%, rgb(13 11 9 / 0.9) 14%, transparent 40%);
  }

  .scene-band.is-top .scene-media::after {
    background:
      linear-gradient(180deg, rgb(13 11 9 / 0.7) 0%, transparent 18%),
      linear-gradient(0deg, var(--bg) 0%, rgb(13 11 9 / 0.9) 14%, transparent 40%);
  }

  .scene-band-body {
    margin-top: calc(-0.12 * 100vw);
  }

  .hero-copy h1 {
    font-size: clamp(2.7rem, 1.3rem + 7.4vw, 4rem);
  }
}

/* ---------- Scene band: a shot with nothing laid over it ---------- */
/* Used where the copy is longer than a hero line: the closing shot of the
   home page and the heads of the secondary pages. The image plays alone, at
   full colour, and the title follows it on the black, overlapping only the
   foot of the shot where it has already faded out. No text ever sits on the
   fox. */
.scene-band {
  height: clamp(400px, 46vw, 82vh);
}

.scene-band::before {
  background: none;
}

.scene-band::after {
  background:
    linear-gradient(180deg, var(--bg) 0%, rgb(13 11 9 / 0.3) 7%, transparent 16%),
    linear-gradient(0deg, var(--bg) 0%, rgb(13 11 9 / 0.82) 12%, transparent 40%);
}

/* At the top of a page the band slides under the transparent nav, and the
   top fade becomes a scrim for the nav links instead of a seam. */
.scene-band.is-top {
  height: clamp(380px, 40vw, 70vh);
  margin-top: calc(-1 * var(--nav-h));
}

.scene-band.is-top::after {
  background:
    linear-gradient(180deg, rgb(13 11 9 / 0.66) 0%, transparent 18%),
    linear-gradient(0deg, var(--bg) 0%, rgb(13 11 9 / 0.82) 12%, transparent 40%);
}

.scene-band-body {
  position: relative;
  z-index: 1;
  margin-top: calc(-1 * clamp(2.5rem, 6vw, 6rem));
}

/* ---------- Marquee: the programme credits ---------- */
/* Exactly one on the page, right under the opening shot: the programmes as a
   moving title strip. The track is duplicated once and moved by -50%, which
   makes the loop seamless; every other name is dimmed for rhythm. */
.marquee {
  --marquee-duration: 70s;
  position: relative;
  overflow: hidden;
  padding-block: clamp(1.1rem, 0.7rem + 1.2vw, 1.75rem);
  border-block: 1px solid var(--line);
  background: var(--bg);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.marquee-track {
  display: flex;
  width: max-content;
  animation: marqueeRun var(--marquee-duration) linear infinite;
}

.marquee:hover .marquee-track {
  animation-play-state: paused;
}

.marquee-group {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 1rem + 2vw, 3rem);
  padding-right: clamp(1.5rem, 1rem + 2vw, 3rem);
}

.marquee-group span {
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 0.9rem + 1.5vw, 2.15rem);
  font-weight: 600;
  letter-spacing: -0.035em;
  color: var(--text);
  white-space: nowrap;
}

.marquee-group span:nth-of-type(even) {
  color: rgb(246 241 234 / 0.36);
}

.marquee-group .dot {
  width: 9px;
  height: 9px;
  rotate: 45deg;
  border-radius: 2px;
  background: var(--accent);
  flex: none;
}

@keyframes marqueeRun {
  to {
    translate: -50% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .marquee-track {
    animation: none;
  }
}

/* ---------- Section head ---------- */
/* Headline, then one short lede, stacked: never a left/right split header,
   and no eyebrow (the hero and the signup carry the only two). Two title
   sizes, by role: .h2 for the story beats (how it works, the group, the
   signup), .h2-s for the supporting sections (the app, the points, FAQ). */
.section-head {
  display: grid;
  gap: var(--s-4);
  max-width: 52ch;
}

.section-head .lede {
  max-width: 46ch;
}

.h2-s {
  font-size: var(--step-3);
  letter-spacing: -0.035em;
  line-height: 1.04;
}

/* ---------- Manifesto ---------- */
/* The problem, alone on the black, between the opening shot and the product.
   Centred on purpose: the sentence is the design. Each word brightens as it
   crosses the reading zone (one CSS view timeline per word); without timeline
   support, or with reduced motion, the line simply reads at full strength. */
.statement {
  --section-air: clamp(5.5rem, 3rem + 9vw, 11rem);
  text-align: center;
  background:
    radial-gradient(46% 52% at 50% 108%, rgb(243 115 10 / 0.2), transparent 72%),
    var(--bg);
  overflow: clip;
}

.statement::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: min(820px, 84vw);
  height: 1px;
  translate: -50% 0;
  background: linear-gradient(90deg, transparent, rgb(243 115 10 / 0.75), transparent);
}

.statement-inner {
  display: grid;
  justify-items: center;
  gap: var(--s-6);
}

.statement blockquote {
  margin: 0;
  max-width: 18ch;
  font-family: var(--font-display);
  font-size: var(--step-5);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.048em;
  color: var(--text);
  text-wrap: balance;
}

.statement-support {
  max-width: 46ch;
  font-size: var(--step-1);
  color: var(--text-body);
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .js [data-scrub] .word-in {
      animation: scrubLight linear both;
      animation-timeline: view(block 0px);
      animation-range: cover 14% cover 42%;
    }

    @keyframes scrubLight {
      from {
        opacity: 0.16;
      }
      to {
        opacity: 1;
      }
    }
  }
}

/* ---------- Pinned sequence: how it works ---------- */
/* The phone stays pinned on its stage while the three steps scroll past, and
   the screen cross-fades to match (site.js sets data-on / data-past). The
   steps hang on a rail whose nodes and segments turn orange as the visitor
   moves through them: progress shown, not numbered. Without JS the rail is
   drawn in full and the steps read as a plain list beside the first screen. */
.sequence {
  display: grid;
  gap: var(--s-7);
  margin-top: var(--s-7);
}

@media (min-width: 900px) {
  .sequence {
    grid-template-columns: minmax(0, 1fr) minmax(0, 500px);
    gap: clamp(2rem, 1rem + 5vw, 6rem);
    align-items: start;
  }

  .sequence-media {
    position: sticky;
    top: calc(var(--nav-h) + clamp(1.25rem, 5vh, 3.5rem));
  }
}

.sequence-steps {
  --step-gap: var(--s-7);
  --rail-x: 0.55rem;
  display: grid;
  gap: var(--step-gap);
  padding-left: 2.75rem;
}

/* The bottom padding keeps the stage pinned while the last step is read:
   without it the end of the sequence pushes the phone under the nav. */
@media (min-width: 900px) {
  .sequence-steps {
    --step-gap: clamp(4.5rem, 20vh, 10rem);
    padding-block: clamp(1.5rem, 7vh, 4rem) clamp(4rem, 26vh, 15rem);
  }
}

.sequence-step {
  position: relative;
  display: grid;
  gap: var(--s-3);
  justify-items: start;
  transition: opacity 0.45s var(--ease);
}

/* Node, then the segment that runs to the next node. */
.sequence-step::before {
  content: "";
  position: absolute;
  left: calc(-2.75rem + var(--rail-x) - 7px);
  top: 0.55em;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid var(--accent);
  background: var(--bg);
  transition: background-color 0.4s var(--ease), border-color 0.4s var(--ease),
    box-shadow 0.4s var(--ease);
}

.sequence-step::after {
  content: "";
  position: absolute;
  left: calc(-2.75rem + var(--rail-x));
  top: calc(0.55em + 18px);
  width: 2px;
  height: calc(100% + var(--step-gap) - 20px);
  border-radius: 2px;
  background:
    linear-gradient(var(--accent), var(--accent)) 0 0 / 100% 100% no-repeat,
    var(--line-strong);
  transition: background-size 0.7s var(--ease);
}

.sequence-step:last-child::after {
  display: none;
}

.js .sequence-step::before {
  border-color: var(--line-strong);
}

.js .sequence-step::after {
  background-size: 100% 0%, auto;
}

.js .sequence-step[data-past="true"]::before,
.js .sequence-step[data-on="true"]::before {
  border-color: var(--accent);
  background: var(--accent);
}

.js .sequence-step[data-on="true"]::before {
  box-shadow: 0 0 0 6px rgb(243 115 10 / 0.2);
}

.js .sequence-step[data-past="true"]::after {
  background-size: 100% 100%, auto;
}

/* Dimmed, not hidden: 0.72 keeps the body copy above 4.5:1 on the black. */
@media (min-width: 900px) {
  .js .sequence-step[data-on="false"] {
    opacity: 0.72;
  }
}

.sequence-step h3 {
  font-size: var(--step-3);
  letter-spacing: -0.035em;
  max-width: 16ch;
}

.sequence-step p {
  max-width: 40ch;
  font-size: var(--step-1);
}

/* The stage: the phone stands in the horizon shot (a landscape-only cut of the
   summit render), the sun's glow pooled at its feet. The blaze gradient under
   it is what shows while the image loads. The stage is the only box that
   clips: the phone runs off its bottom edge, and its shadow falls on the
   landscape instead of being cut into a hard rectangle by an inner box. */
.stage {
  position: relative;
  isolation: isolate;
  display: grid;
  height: clamp(500px, 76vh, 780px);
  padding: clamp(2rem, 4vw, 3.25rem) clamp(1.25rem, 3vw, 2.5rem) 0;
  border-radius: var(--r-media);
  overflow: clip;
  background: linear-gradient(160deg, var(--blaze-mid), var(--blaze-lo));
  box-shadow: var(--shadow-3), inset 0 1px 0 rgb(255 255 255 / 0.16);
}

.stage-art {
  position: absolute;
  inset: 0;
  z-index: -2;
}

.stage-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 26%;
}

.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(64% 42% at 50% 104%, rgb(245 128 34 / 0.7), transparent 72%),
    linear-gradient(180deg, rgb(13 11 9 / 0.3) 0%, transparent 28%, rgb(87 28 2 / 0.4) 100%);
  pointer-events: none;
}

/* The three screens share one cell. Each is a column whose phone takes the
   free space above it (margin-top: auto): with room to spare it stands on the
   stage's bottom edge, and when the stage is shorter than the phone it hangs
   from the top and runs off the bottom, so its status bar is never cut. */
.sequence-screens {
  display: grid;
  min-height: 0;
}

.sequence-screens > * {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 0;
}

.sequence-screen .device {
  flex: none;
  margin-top: auto;
  width: min(330px, 86%);
}

/* Without JS only the first screen shows: it is the one step 01 describes. */
html:not(.js) .sequence-screen + .sequence-screen {
  display: none;
}

.js .sequence-screen {
  opacity: 0;
  scale: 0.97;
  transition: opacity 0.5s var(--ease), scale 0.5s var(--ease);
  pointer-events: none;
}

.js .sequence-screen[data-on="true"] {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}

@media (max-width: 899px) {
  .stage {
    height: clamp(420px, 118vw, 600px);
  }
}

/* ---------- Device: the iPhone around the App Store captures ---------- */
/* Real captures only (tools/build-ios.py: the release app on iOS, status bar
   included, every image loaded). The frame adds what a screenshot cannot
   carry: the titanium edge and the Dynamic Island, which the capture pipeline
   erased. `.device-bleed` drops the bottom of the frame: the screen runs off
   the edge of the box it stands in, as on the App Store boards. */
.device {
  --bezel: 9px;
  --screen-r: 44px;
  position: relative;
  width: min(340px, 100%);
  margin-inline: auto;
  padding: var(--bezel);
  border-radius: calc(var(--screen-r) + var(--bezel));
  background: linear-gradient(145deg, #4a423b 0%, #191512 34%, #0b0908 62%, #2c2621 100%);
  box-shadow:
    inset 0 0 0 1px rgb(255 255 255 / 0.16),
    inset 0 0 0 2.5px #050403,
    0 2px 6px rgb(0 0 0 / 0.4),
    0 40px 90px rgb(0 0 0 / 0.5);
  container-type: inline-size;
}

/* The Dynamic Island, centred on the status bar's line. */
.device::before {
  content: "";
  position: absolute;
  z-index: 1;
  top: calc(var(--bezel) + 3.1cqi);
  left: 50%;
  width: 28.6cqi;
  height: 8.4cqi;
  translate: -50% 0;
  border-radius: 999px;
  background: #050403;
}

.device picture {
  display: block;
}

.device img {
  width: 100%;
  border-radius: var(--screen-r);
  background: #fff;
}

.device-bleed {
  padding-bottom: 0;
  border-end-start-radius: 0;
  border-end-end-radius: 0;
}

.device-bleed img {
  border-end-start-radius: 0;
  border-end-end-radius: 0;
}

/* ---------- Bento: what is in the app ---------- */
/* Six pieces of the product, six cells, each on its own material: the
   session screen in a phone over a horizon glow, the one flat-orange tile,
   graphite tiles carrying the app's own widgets (rebuilt from its code, see
   .appui), and the group chat over the forest cut. Tiles answer a fine
   pointer with a soft spotlight (site.js writes --sx / --sy); touch and
   reduced motion get the plain tile. */
.bento {
  display: grid;
  gap: clamp(0.85rem, 0.5rem + 1vw, 1.25rem);
  margin-top: var(--s-7);
  grid-template-columns: 1fr;
}

.tile {
  --tile-pad: clamp(1.4rem, 1rem + 1.2vw, 2.1rem);
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding: var(--tile-pad);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background:
    linear-gradient(180deg, rgb(255 236 214 / 0.05), rgb(255 236 214 / 0.012)),
    var(--surface);
  box-shadow: inset 0 1px 0 rgb(255 236 214 / 0.05);
  overflow: clip;
  container-type: inline-size;
  transition: translate 0.3s var(--ease), box-shadow 0.3s var(--ease), border-color 0.3s var(--ease);
}

.tile:hover {
  translate: 0 -3px;
  border-color: var(--line-strong);
  box-shadow: var(--shadow-2), inset 0 1px 0 rgb(255 236 214 / 0.07);
}

.tile::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: radial-gradient(380px circle at var(--sx, 50%) var(--sy, 0%), rgb(255 162 92 / 0.12), transparent 62%);
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  pointer-events: none;
}

.tile:hover::after {
  opacity: 1;
}

.tile > :not(.tile-art) {
  position: relative;
  z-index: 1;
}

.tile h3 {
  font-size: var(--step-2);
  letter-spacing: -0.025em;
}

.tile p {
  max-width: 42ch;
}

.tile-text {
  display: grid;
  gap: 0.6rem;
}

/* Copy and widget side by side once the tile is wide enough (leaderboard,
   streak, chat); stacked below that, the widget at the foot of the tile. */
.tile-split {
  display: grid;
  flex: 1;
  align-content: space-between;
  gap: var(--s-5);
}

@container (min-width: 600px) {
  .tile-split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    align-items: center;
    gap: clamp(1.5rem, 4cqi, 3rem);
  }
}

/* The session screen, in a phone that runs off the bottom of the tile. */
.tile-shot {
  padding-bottom: 0;
  background:
    radial-gradient(110% 70% at 50% 115%, rgb(243 115 10 / 0.42), transparent 62%),
    linear-gradient(180deg, rgb(255 236 214 / 0.05), rgb(255 236 214 / 0.012)),
    var(--surface);
}

.tile-shot .device {
  flex: none;
  margin-top: auto;
  width: min(300px, 84%);
  transition: translate 0.55s var(--ease);
}

.tile-shot:hover .device {
  translate: 0 -10px;
}

/* The one orange tile: brand orange carrying ink, never white. */
.tile-accent {
  background: linear-gradient(160deg, #f88227, var(--accent) 45%, #e2650a);
  border-color: transparent;
  color: var(--on-accent);
}

.tile-accent::after {
  background: radial-gradient(380px circle at var(--sx, 50%) var(--sy, 0%), rgb(255 255 255 / 0.22), transparent 62%);
}

.tile-accent h3,
.tile-accent .tile-text p {
  color: var(--on-accent);
}

.tile-accent .tile-text p {
  opacity: 0.86;
}

/* A tile that carries one of the app's widgets: copy on top, the widget
   standing at the foot of the tile, centred. */
.tile-appui > .appui {
  margin-top: auto;
  align-self: center;
}

.tile-split > .appui {
  justify-self: center;
}

/* The chat tile carries the forest cut beside its copy, never under it: the
   image fills the right of the tile (the top on a phone) and melts into the
   graphite, and the chat card sits on it as an opaque app card. */
.tile-photo {
  min-height: 15rem;
}

.tile-art {
  position: absolute;
  inset: 0 0 0 38%;
  z-index: -1;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 38%);
  mask-image: linear-gradient(90deg, transparent, #000 38%);
}

.tile-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 32%;
}

@media (max-width: 719px) {
  .tile-photo {
    padding-top: calc(var(--tile-pad) + clamp(9rem, 32vw, 13rem));
  }

  .tile-art {
    inset: 0 0 auto 0;
    height: calc(var(--tile-pad) + clamp(10rem, 36vw, 14rem));
    -webkit-mask-image: linear-gradient(180deg, #000 50%, transparent);
    mask-image: linear-gradient(180deg, #000 50%, transparent);
  }
}

/* 6 tiles -> 6 cells at every breakpoint, no orphan. Two columns: the phone
   beside the two stats cards, then the leaderboard, the streak and the chat
   across, copy beside widget. Twelve columns: the phone on two rows, the
   leaderboard across the top right, the stats pair under it, the streak and
   the chat on the last row. */
@media (min-width: 720px) and (max-width: 1039px) {
  .bento {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .bento > .tile-shot { grid-column: 1; grid-row: 1 / 3; }
  .bento > .tile-points { grid-column: 2; grid-row: 1; }
  .bento > .tile-defi { grid-column: 2; grid-row: 2; }
  .bento > .tile-board { grid-column: 1 / -1; grid-row: 3; }
  .bento > .tile-streak { grid-column: 1 / -1; grid-row: 4; }
  .bento > .tile-photo { grid-column: 1 / -1; grid-row: 5; }
}

@media (min-width: 1040px) {
  .bento {
    grid-template-columns: repeat(12, minmax(0, 1fr));
  }
  .bento > .tile-shot { grid-column: 1 / 6; grid-row: 1 / 3; min-height: 36rem; }
  .bento > .tile-board { grid-column: 6 / 13; grid-row: 1; }
  .bento > .tile-points { grid-column: 6 / 10; grid-row: 2; }
  .bento > .tile-defi { grid-column: 10 / 13; grid-row: 2; }
  .bento > .tile-streak { grid-column: 1 / 6; grid-row: 3; }
  .bento > .tile-photo { grid-column: 6 / 13; grid-row: 3; }
}

/* ---------- Group: the group's own header, rebuilt ---------- */
/* The top of the app's group screen, out of the phone: the group banner and
   the weekly goal card sliding out from under it, overlapped by 18px as in
   the app (GroupLayout.sheetOverlap). The goal fills live (site.js), the
   members in a fixed order, never re-sorted: a shared target, not a ranking.
   The pair leans toward a fine pointer ([data-tilt]). */
.split {
  display: grid;
  gap: clamp(2.5rem, 1rem + 4vw, 6rem);
  align-items: center;
}

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

.split-media {
  position: relative;
}

.split-stage {
  position: relative;
  display: grid;
  place-items: center;
  min-height: clamp(420px, 58vh, 580px);
  padding: clamp(2.25rem, 5vw, 4rem) clamp(1.1rem, 4vw, 3rem);
  border: 1px solid var(--line);
  border-radius: var(--r-media);
  background:
    radial-gradient(70% 45% at 50% 108%, rgb(255 162 92 / 0.5), rgb(243 115 10 / 0.24) 45%, transparent 75%),
    linear-gradient(180deg, var(--surface-2), var(--surface));
  overflow: clip;
}

.split-body h2 {
  margin-bottom: var(--s-5);
}

.split-body > p + p {
  margin-top: var(--s-4);
}

.split-body .form {
  margin-top: var(--s-6);
}

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

.feature-list li {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: var(--s-3);
  align-items: start;
}

.feature-list .icon {
  width: 20px;
  height: 20px;
  margin-top: 0.2em;
  color: var(--accent-text);
}

.feature-list strong {
  color: var(--text);
  font-weight: 600;
}

/* ---------- Points: the figures, then what Tribos refuses ---------- */
/* The four constants of the points system as large figures on the black, no
   card: they are the claim. Then the one list a competitor cannot copy, as a
   two-column statement with the single "yes" in orange. */
.stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: var(--s-7);
  border-top: 1px solid var(--line-strong);
}

@media (min-width: 860px) {
  .stats {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .stats li + li {
    padding-left: var(--s-6);
    border-left: 1px solid var(--line);
  }
}

@media (max-width: 859px) {
  .stats li:nth-child(even) {
    padding-left: var(--s-5);
    border-left: 1px solid var(--line);
  }

  .stats li:nth-child(n + 3) {
    border-top: 1px solid var(--line);
  }
}

.stats li {
  display: grid;
  align-content: start;
  gap: var(--s-3);
  padding: var(--s-6) var(--s-4) var(--s-5) 0;
}

.stats .icon {
  width: 22px;
  height: 22px;
  color: var(--accent);
}

.stats b {
  font-family: var(--font-display);
  font-size: var(--step-5);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.stats span {
  font-size: var(--step--1);
  color: var(--text-muted);
}

.stats-note {
  margin-top: var(--s-5);
  max-width: 56ch;
}

.nots-split {
  display: grid;
  gap: var(--s-6);
  margin-top: clamp(4rem, 2rem + 6vw, 7rem);
  padding-top: var(--s-7);
  border-top: 1px solid var(--line);
}

@media (min-width: 860px) {
  .nots-split {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: clamp(2rem, 5vw, 5rem);
  }
}

.nots-split h3 {
  font-size: var(--step-3);
  letter-spacing: -0.035em;
  max-width: 12ch;
}

.nots {
  display: grid;
}

.nots li {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 0.9rem;
  align-items: baseline;
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--line);
  font-size: var(--step-1);
}

.nots li:first-child {
  padding-top: 0;
}

.nots li:last-child {
  border-bottom: 0;
  color: var(--text);
}

.nots li .icon {
  width: 18px;
  height: 18px;
  color: var(--text-muted);
  translate: 0 0.15em;
}

.nots li:last-child .icon {
  color: var(--accent);
}

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

/* ---------- Blaze: the orange poster surface ---------- */
/* The old site's signature, kept on purpose: a deep orange gradient. Two
   variants, both limited to a few moments per page:
     .blaze             hot in one corner (bottom right): posters whose copy
                        sits top-left and whose hot corner takes the button;
     .blaze.blaze-deep  even and deep, a soft sheen at the top: panels full
                        of small text (the signup, the iPhone install box).
   Forms inside a blaze remap the --panel-* tokens (see Forms). */
.blaze {
  --panel-fg: var(--on-blaze);
  --panel-fg-dim: var(--on-blaze-dim);
  --panel-line: rgb(255 255 255 / 0.44);
  --panel-line-hover: rgb(255 255 255 / 0.72);
  --panel-input: rgb(40 12 0 / 0.34);
  --panel-placeholder: rgb(255 255 255 / 0.76);
  --panel-help: var(--on-blaze-dim);
  --panel-link: var(--on-blaze);
  position: relative;
  isolation: isolate;
  overflow: clip;
  color: var(--on-blaze);
  background:
    radial-gradient(75% 95% at 100% 100%, rgb(245 128 34 / 0.95), transparent 60%),
    radial-gradient(60% 70% at 0% 0%, rgb(42 14 1 / 0.55), transparent 70%),
    linear-gradient(152deg, var(--blaze-mid) 0%, var(--blaze-deep) 50%, var(--blaze-lo) 100%);
  box-shadow: var(--shadow-3), inset 0 1px 0 rgb(255 255 255 / 0.2);
}

.blaze-deep {
  background:
    radial-gradient(90% 60% at 85% -10%, rgb(245 128 34 / 0.26), transparent 65%),
    linear-gradient(160deg, #b23d0a 0%, #8c3106 52%, #521a02 100%);
}

.blaze :focus-visible {
  outline-color: var(--text);
}

/* ---------- Band (entreprises) ---------- */
.band {
  padding: clamp(2.25rem, 1.4rem + 4vw, 5rem);
  border-radius: var(--r-media);
}

.band-inner {
  position: relative;
  display: grid;
  gap: var(--s-6);
  align-items: end;
}

@media (min-width: 860px) {
  .band-inner {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: clamp(2rem, 5vw, 4rem);
  }
}

.band h2 {
  max-width: 13ch;
  font-size: var(--step-4);
  letter-spacing: -0.045em;
  line-height: 1;
  color: var(--on-blaze);
}

.band-inner h2 + p,
.band-inner p + p {
  margin-top: var(--s-4);
}

.band p {
  max-width: 48ch;
  font-size: var(--step-1);
  color: var(--on-blaze-dim);
}

/* On a phone the band stacks and the hot corner would reach the copy: the
   band runs deep there. */
@media (max-width: 859px) {
  .band {
    background:
      radial-gradient(90% 40% at 100% 100%, rgb(245 128 34 / 0.6), transparent 70%),
      linear-gradient(160deg, #b23d0a 0%, #8c3106 52%, #521a02 100%);
  }
}

/* ---------- FAQ: heading column + answers ---------- */
/* Two columns on desktop: the title stays pinned on the left while the
   answers open on the right. <details> is the source of truth, so find in
   page and no-JS behave natively. .faq also stands alone (support.html). */
.faq-split {
  display: grid;
  gap: var(--s-6);
}

@media (min-width: 900px) {
  .faq-split {
    grid-template-columns: minmax(0, 4fr) minmax(0, 7fr);
    gap: clamp(2rem, 6vw, 6rem);
    align-items: start;
  }

  .faq-head {
    position: sticky;
    top: calc(var(--nav-h) + 2rem);
  }
}

.faq-more {
  margin-top: var(--s-5);
}

.faq-more a {
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.faq {
  display: grid;
  margin-top: var(--s-6);
  border-top: 1px solid var(--line-strong);
}

.faq-split .faq {
  margin-top: 0;
}

.faq details {
  border-bottom: 1px solid var(--line);
}

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: clamp(1.2rem, 0.95rem + 0.6vw, 1.65rem) 0;
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
  cursor: pointer;
  list-style: none;
  transition: color 0.2s var(--ease);
}

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

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

.faq summary .chev {
  flex: none;
  width: 30px;
  height: 30px;
  padding: 6px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  color: var(--text-muted);
  transition: rotate 0.3s var(--ease), color 0.2s var(--ease), background-color 0.2s var(--ease),
    border-color 0.2s var(--ease);
}

.faq details[open] summary .chev {
  rotate: 45deg;
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.faq .answer {
  max-width: 64ch;
  padding: 0 0 var(--s-5);
}

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

.faq .answer a {
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

@supports (interpolate-size: allow-keywords) {
  :root {
    interpolate-size: allow-keywords;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .faq .answer {
    animation: answerIn 0.34s var(--ease);
  }

  @keyframes answerIn {
    from {
      opacity: 0;
      translate: 0 -6px;
    }
  }
}

/* ---------- Finale: the closing shot and the signup ---------- */
/* The last shot plays alone, full width (.scene-band), then the signup rises
   over its foot on a deep blaze panel: the sun of the image turning into the
   orange of the brand. Promise and form side by side, reassurance below. */
.finale {
  position: relative;
  padding-bottom: clamp(4rem, 2rem + 6vw, 8rem);
}

.finale-scene .scene-media {
  --focus: 60% 3%;
}

.finale-body {
  position: relative;
  z-index: 1;
  margin-top: calc(-1 * clamp(4rem, 9vw, 9rem));
}

.signup {
  display: grid;
  gap: var(--s-7);
  padding: clamp(1.75rem, 1rem + 3.5vw, 4rem);
  border-radius: var(--r-media);
}

@media (min-width: 960px) {
  .signup {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    grid-template-areas:
      "head form"
      "support support";
    gap: clamp(2.5rem, 4vw, 4rem) clamp(2.5rem, 5vw, 5.5rem);
    align-items: start;
  }

  .signup-head { grid-area: head; }
  .signup .form { grid-area: form; }
  .signup-support { grid-area: support; }
}

.signup-head .eyebrow {
  color: var(--on-blaze);
}

.signup-head .eyebrow .pulse,
.signup-head .eyebrow .pulse::after {
  background: var(--on-blaze);
}

.signup-head h2 {
  max-width: 13ch;
  font-size: var(--step-4);
  letter-spacing: -0.047em;
  line-height: 0.98;
  color: var(--on-blaze);
}

.signup-head .lede {
  max-width: 38ch;
  margin-top: var(--s-5);
  color: var(--on-blaze-dim);
}

.signup-support {
  display: grid;
  gap: var(--s-6);
  padding-top: var(--s-6);
  border-top: 1px solid var(--blaze-line);
}

@media (min-width: 760px) {
  .signup-support {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-6) clamp(2rem, 5vw, 4rem);
  }
}

.signup-support h3 {
  margin-bottom: var(--s-4);
  font-family: var(--font-text);
  font-size: var(--step-0);
  font-weight: 600;
  letter-spacing: 0;
  color: var(--on-blaze);
}

.assurances,
.signup-next {
  display: grid;
  gap: var(--s-4);
}

.assurances li,
.signup-next li {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr);
  gap: 0.8rem;
  align-items: start;
  font-size: var(--step--1);
  color: var(--panel-fg-dim);
}

.assurances .icon,
.signup-next .icon {
  width: 18px;
  height: 18px;
  margin-top: 0.22em;
  color: var(--panel-fg);
}

.assurances strong,
.signup-next strong {
  color: var(--panel-fg);
}

@media (max-width: 759px), (orientation: portrait) and (max-width: 1199px) {
  .finale-body {
    margin-top: calc(-0.1 * 100vw);
  }
}

/* ---------- Forms ---------- */
.form {
  display: grid;
  gap: var(--s-3);
}

.field {
  display: grid;
  gap: 0.45rem;
}

.field label {
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--panel-fg);
}

.field input {
  width: 100%;
  min-height: 58px;
  padding: 0 1.3rem;
  border: 1px solid var(--panel-line);
  border-radius: var(--r-pill);
  background: var(--panel-input);
  color: var(--panel-fg);
  transition: border-color 0.2s var(--ease), background-color 0.2s var(--ease);
}

.field input::placeholder {
  color: var(--panel-placeholder);
  opacity: 1;
}

.field input:hover {
  border-color: var(--panel-line-hover);
}

.field input:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent);
}

.field .help,
.form .help {
  font-size: var(--step--1);
  color: var(--panel-help);
}

.field .error {
  display: none;
  font-size: var(--step--1);
  font-weight: 600;
  color: #ffb4a1;
}

.field[data-invalid="true"] input {
  border-color: #ff7a5c;
}

.field[data-invalid="true"] .error {
  display: block;
}

.form .btn {
  width: 100%;
}

/* Kept in the render tree at all times: a live region that is display:none
   when its text changes is announced unreliably. Empty means zero height. */
.form-status {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  border-radius: var(--r-card);
  font-size: var(--step--1);
  font-weight: 500;
}

.form-status[data-state] {
  padding: 0.9rem 1.1rem;
}

.form-status[data-state="error"] {
  background: #3a1f18;
  color: #ffc3b1;
}

.form-status[data-state="sending"] {
  background: var(--surface-2);
  color: var(--text-body);
}

.form-status[data-state="ok"] {
  background: #1e2e21;
  color: #a9e6bb;
}

.honeypot {
  position: absolute;
  left: -9999px;
  opacity: 0;
  pointer-events: none;
}

/* Inline signup: the field and the button read as one pill, the button
   sitting inside the field's right end, as in the reference hero. */
.form-inline {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: 0;
}

.form-inline .field {
  grid-column: 1 / -1;
  grid-row: 1;
  gap: 0;
}

.form-inline .field input {
  padding-right: 15rem;
  background: rgb(8 6 5 / 0.6);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

.form-inline .field .error {
  padding: 0.6rem 1.3rem 0;
}

.form-inline .btn {
  grid-column: 2;
  grid-row: 1;
  z-index: 1;
  width: auto;
  min-height: 48px;
  margin: 5px;
  padding-inline: 1.5rem;
}

.form-inline .form-status,
.form-inline .help,
.form-inline .consent-note {
  grid-column: 1 / -1;
  margin-top: var(--s-2);
}


@media (max-width: 620px) {
  .form-inline {
    grid-template-columns: 1fr;
    row-gap: var(--s-3);
  }
  .form-inline .field input {
    padding-right: 1.3rem;
  }
  .form-inline .btn {
    grid-column: 1;
    grid-row: 2;
    width: 100%;
    min-height: 54px;
    margin: 0;
  }
}

/* ---------- Platform chips ---------- */
/* Nothing is pre-selected: pre-checking Android would poison the one datum
   the field exists to collect. */
.chips {
  border: 0;
  padding: 0;
  margin: 0 0 var(--s-4);
  display: grid;
  gap: var(--s-3);
}

.chips legend {
  padding: 0;
  margin-bottom: var(--s-3);
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--panel-fg);
}

.chips-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.chip input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.chip span {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 46px;
  padding-inline: 1.2rem;
  border: 1px solid var(--panel-line);
  border-radius: var(--r-pill);
  color: var(--panel-fg);
  font-size: var(--step--1);
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s var(--ease), border-color 0.2s var(--ease), color 0.2s var(--ease);
}

.chip:hover span {
  border-color: var(--panel-line-hover);
}

.chip input:checked + span {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
}

.chip input:focus-visible + span {
  outline: 3px solid var(--accent-text);
  outline-offset: 2px;
}

.consent-note {
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--panel-help);
}

.consent-note a {
  color: var(--panel-link);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------- Live eyebrow ---------- */
/* The one status indicator on the page: the beta wave is open or it is not.
   The dot sits on the first line, so a wrapped eyebrow keeps it in place. */
.eyebrow-live {
  align-items: flex-start;
}

.eyebrow-live .pulse {
  position: relative;
  width: 8px;
  height: 8px;
  margin-top: 0.42em;
  margin-top: calc((1lh - 8px) / 2);
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

@media (prefers-reduced-motion: no-preference) {
  .eyebrow-live .pulse::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--accent);
    animation: pulseHalo 2.4s ease-out infinite;
  }

  @keyframes pulseHalo {
    from {
      opacity: 0.6;
      scale: 1;
    }
    to {
      opacity: 0;
      scale: 3.2;
    }
  }
}

/* ---------- Sticky signup dock (small screens) ---------- */
/* A link, never an input: a fixed input fights the iOS keyboard. */
.cta-dock {
  position: fixed;
  inset: auto 0 0;
  z-index: 44;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: 0.7rem var(--gutter);
  padding-bottom: calc(0.7rem + env(safe-area-inset-bottom, 0px));
  background: rgb(13 11 9 / 0.86);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
  translate: 0 130%;
  visibility: hidden;
  transition: translate 0.35s var(--ease), visibility 0s linear 0.35s;
}

.cta-dock[data-show="true"] {
  translate: 0 0;
  visibility: visible;
  transition-delay: 0s;
}

.cta-dock-line {
  font-size: var(--step--1);
  color: var(--text-muted);
  line-height: 1.3;
}

.cta-dock .btn {
  flex: none;
}

@media (min-width: 900px) {
  .cta-dock {
    display: none;
  }
}

@media (max-width: 899px) {
  .footer-bottom {
    padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  }
}

/* ---------- Footer ---------- */
/* Quiet columns, then the wordmark set huge and almost black on black: the
   brand closes the page, without asking for attention. */
.footer {
  position: relative;
  border-top: 1px solid var(--line);
  padding-block: clamp(3rem, 2rem + 3vw, 5rem) var(--s-6);
  background: var(--bg);
  overflow: clip;
}

.footer-grid {
  display: grid;
  gap: clamp(2rem, 1rem + 3vw, 3.5rem);
}

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

.footer-brand p {
  margin-top: var(--s-4);
  max-width: 30ch;
  font-size: var(--step--1);
  color: var(--text-muted);
}

.footer .footer-heading {
  margin-bottom: var(--s-4);
  font-family: var(--font-text);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0;
  color: var(--text);
}

.footer-links {
  display: grid;
  font-size: var(--step--1);
}

.footer-links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--text-body);
  transition: color 0.2s var(--ease);
}

.footer-links a:hover {
  color: var(--accent-text);
}

.footer-mark {
  margin: clamp(2.5rem, 2rem + 3vw, 4.5rem) 0 0;
  font-family: var(--font-display);
  font-size: clamp(4.5rem, 1rem + 21vw, 21rem);
  font-weight: 800;
  line-height: 0.8;
  letter-spacing: -0.065em;
  text-align: center;
  white-space: nowrap;
  color: rgb(246 241 234 / 0.05);
  user-select: none;
  pointer-events: none;
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  margin-top: clamp(1.5rem, 1rem + 2vw, 3rem);
  padding-top: var(--s-5);
  border-top: 1px solid var(--line);
  font-size: var(--step--1);
  color: var(--text-muted);
}

/* =========================================================================
   Secondary pages
   ========================================================================= */

/* ---------- Page head under a scene band (entreprises, merci, 404) ---------- */
.page-hero {
  position: relative;
  padding-bottom: clamp(3rem, 2rem + 4vw, 5.5rem);
}

.page-head {
  max-width: 48rem;
}

.page-head h1 {
  font-size: var(--step-5);
  letter-spacing: -0.047em;
  line-height: 0.98;
  margin-bottom: var(--s-5);
}

/* A long sentence for a title (merci) takes the smaller display step. */
.page-head .h1 {
  font-size: var(--step-4);
}

.page-head .lede {
  max-width: 44ch;
}

.next {
  margin-top: var(--s-7);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3) var(--s-5);
  margin-top: var(--s-6);
}

/* ---------- Feature rows (entreprises) ---------- */
/* Three reasons, read as rows: icon, title, then the sentence, on one
   hairline each. Not three equal cards. */
.feature-rows {
  display: grid;
  margin-top: var(--s-7);
  border-top: 1px solid var(--line-strong);
}

.feature-row {
  display: grid;
  grid-template-columns: 2.75rem minmax(0, 1fr);
  gap: var(--s-3) var(--s-5);
  align-items: start;
  padding-block: clamp(1.5rem, 1rem + 1.5vw, 2.25rem);
  border-bottom: 1px solid var(--line);
}

.feature-row .icon {
  width: 30px;
  height: 30px;
  color: var(--accent);
}

.feature-row h3 {
  font-size: var(--step-2);
  letter-spacing: -0.025em;
}

.feature-row p {
  grid-column: 2;
  max-width: 52ch;
}

@media (min-width: 860px) {
  .feature-row {
    grid-template-columns: 3.5rem minmax(0, 5fr) minmax(0, 6fr);
    gap: clamp(1.5rem, 4vw, 4rem);
  }

  .feature-row p {
    grid-column: 3;
    font-size: var(--step-1);
  }
}

/* ---------- Timeline (entreprises) ---------- */
/* The same rail as the home page's sequence, laid flat: three connected
   steps, vertical on a phone, horizontal from 860px. */
.timeline {
  --rail-x: 0.55rem;
  position: relative;
  display: grid;
  gap: var(--s-7);
  margin-top: var(--s-7);
  padding-left: 2.75rem;
}

.timeline::before {
  content: "";
  position: absolute;
  left: var(--rail-x);
  top: 0.6rem;
  bottom: 0.6rem;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--accent), rgb(243 115 10 / 0.18));
}

.timeline-step {
  position: relative;
  display: grid;
  align-content: start;
  gap: var(--s-3);
}

.timeline-step::before {
  content: "";
  position: absolute;
  left: calc(-2.75rem + var(--rail-x) - 7px);
  top: 0.4em;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 6px rgb(243 115 10 / 0.18);
}

.timeline-step h3 {
  font-size: var(--step-2);
  letter-spacing: -0.025em;
}

.timeline-step p {
  max-width: 40ch;
}

@media (min-width: 860px) {
  .timeline {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(1.5rem, 4vw, 3.5rem);
    padding-left: 0;
    padding-top: 3rem;
  }

  .timeline::before {
    left: 0;
    right: 0;
    top: 0.45rem;
    bottom: auto;
    width: auto;
    height: 2px;
    background: linear-gradient(90deg, var(--accent), rgb(243 115 10 / 0.18));
  }

  .timeline-step::before {
    left: 0;
    top: calc(-3rem + 0.45rem - 7px);
  }
}

/* ---------- Confirmation page ---------- */
/* The iPhone install box is the one action that is already open: it takes a
   deep blaze surface (.blaze.blaze-deep in the markup). The invite stays on
   graphite. */
.testflight,
.invite {
  display: grid;
  gap: var(--s-5);
  margin-top: var(--s-6);
  padding: clamp(1.4rem, 0.9rem + 2vw, 2.4rem);
  border-radius: var(--r-media);
}

.invite {
  border: 1px solid var(--glass-line);
  background: linear-gradient(180deg, rgb(28 24 21 / 0.9), rgb(20 17 15 / 0.96));
}

.testflight[hidden] {
  display: none;
}

.testflight h2 {
  margin-bottom: var(--s-2);
}

@media (min-width: 760px) {
  .testflight {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
  }
}

.invite-body h2 {
  margin-bottom: var(--s-3);
}

.invite-body > p {
  max-width: 54ch;
}

.share-rack {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-5);
}

.share-rack .btn .icon {
  width: 18px;
  height: 18px;
}

.share-rack .btn[data-copied="true"] {
  border-color: var(--accent);
  color: var(--accent-text);
}

.share-msg {
  margin-top: var(--s-5);
  padding: var(--s-4) var(--s-5);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-card);
  background: rgb(8 6 5 / 0.5);
  font-size: var(--step--1);
  color: var(--text-body);
}

.share-msg a {
  color: var(--accent-text);
}

/* The copy button is useless without JS. */
html:not(.js) [data-needs-js] {
  display: none;
}

.invite-form {
  margin-top: var(--s-7);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}

.invite-form .chips legend {
  font-size: var(--step-0);
}

.invite-form .btn {
  justify-self: start;
  width: auto;
}

.invite-form[data-done="true"] .chips,
.invite-form[data-done="true"] .btn,
.invite-form[data-done="true"] .help {
  display: none;
}

/* The confirmation page has no dock: the visitor already converted. */
.cta-dock:not([data-show]) {
  visibility: hidden;
}

/* ---------- Legal / article pages ---------- */
.doc {
  max-width: 70ch;
  margin-top: var(--s-7);
}

.doc h2 {
  font-size: var(--step-2);
  letter-spacing: -0.025em;
  margin-top: var(--s-8);
  margin-bottom: var(--s-4);
}

.doc h2:first-of-type {
  margin-top: var(--s-6);
}

.doc h3 {
  font-size: var(--step-1);
  margin-top: var(--s-6);
  margin-bottom: var(--s-3);
}

.doc p + p,
.doc ul,
.doc ul + p {
  margin-top: var(--s-4);
}

.doc ul {
  display: grid;
  gap: 0.6rem;
  padding-left: 1.15rem;
  list-style: disc;
}

.doc li::marker {
  color: var(--accent);
}

.doc strong {
  color: var(--text);
}

.doc a {
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.doc-meta {
  margin-top: var(--s-4);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-muted);
}

.doc-actions {
  max-width: 70ch;
  margin-top: var(--s-7);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}

/* =========================================================================
   .appui - the app's own widgets, rebuilt from its code

   The widgets of the app, not an interpretation: every value below comes
   from the app repo (lib/core/app_theme.dart and the widget file named on
   each block), in the app's logical pixels. Same fonts (Montserrat titles,
   Roboto body, declared just below), same colours, radii and gaps. Only two things are
   the site's: a deeper shadow (the app's 6 % shadow vanishes on black) and
   the entrance motion.

   Weights are the ones the app SHOWS, read off the App Store captures, not
   the ones its code asks for. A Roboto style made bold with copyWith() keeps
   google_fonts' single-weight family, so "Points x2", "100", the leaderboard
   names and the chat names ship at the base weight (400 or 500). Montserrat
   styles built with an explicit weight render as written.

   A widget is a picture of the app: role="img" plus an aria-label saying what
   it shows. The site's AA rule applies to the copy around it, which states
   the point in words; inside, the app's colours stand as they ship. The defi
   blue and the goal green live here and nowhere else on the site.
   ========================================================================= */
/* The app's fonts, for these widgets only. Declared here rather than in
   fonts.css: /assets/fonts/* is cached immutable for a year (netlify.toml), so
   a changed fonts.css would never reach a returning visitor, while new woff2
   names are safe. Built by tools/build-fonts.py (google/fonts, OFL, see
   assets/fonts/OFL-*.txt), latin subset, variable weights. */
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 500 900;
  font-display: swap;
  src: url("../fonts/montserrat-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+2248, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Roboto";
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url("../fonts/roboto-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+2248, U+FEFF, U+FFFD;
}

.appui {
  --ui-ink: #2c3a43;          /* textDark */
  --ui-medium: #374151;       /* textMedium */
  --ui-muted: #6b7280;        /* textMuted */
  --ui-light: #9ca3af;        /* textLight */
  --ui-variant: #f3f4f6;      /* surfaceVariant */
  --ui-border: #e5e7eb;       /* border */
  --ui-orange: #f3730a;       /* primary */
  --ui-orange-soft: #ffeed0;  /* primaryLight */
  --ui-blue: #4499f2;         /* challengeBlue */
  --ui-blue-ink: #2385ea;
  --ui-blue-empty: #b4d6f9;
  --ui-green: #2e7d2a;        /* the goal's "+1": a gain, not a score */
  --ui-title: "Montserrat", var(--font-text);
  --ui-body: "Roboto", var(--font-text);
  --ui-lift: 0 2px 6px rgb(0 0 0 / 0.24), 0 22px 46px rgb(0 0 0 / 0.42);
  position: relative;
  color: var(--ui-ink);
  font-family: var(--ui-body);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: normal;
  text-align: start;
}

.appui p {
  max-width: none;
}

/* One-line labels, as Flutter's maxLines: 1. */
.ui-band-text,
.ui-stat-foot,
.ui-name,
.ui-pts,
.ui-more,
.ui-chip-name,
.ui-msg-head,
.ui-input,
.ui-banner-name {
  white-space: nowrap;
}

.appui picture {
  display: contents;
}

/* AppCard.elevated: white, 16px radius. */
.ui-card {
  background: #fff;
  border-radius: 16px;
  box-shadow: var(--ui-lift);
}

/* MemberAvatar: the photo, or the pseudo's initial on the app's grey. */
.ui-ava {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--ui-variant);
  object-fit: cover;
}

span.ui-ava {
  display: grid;
  place-items: center;
  color: var(--ui-ink);
  font-family: var(--ui-title);
  font-size: 14.4px;
  font-weight: 600;
  line-height: 1;
}

@keyframes uiPop {
  from {
    opacity: 0;
    scale: 0.4;
  }
  62% {
    scale: 1.12;
  }
}

@keyframes uiRise {
  from {
    opacity: 0;
    translate: 0 10px;
  }
}

@keyframes uiGrow {
  from {
    scale: 1 0;
  }
}

/* ---------- Streak (lib/screens/home/widgets/streak_card.dart) ---------- */
.ui-streak {
  display: flex;
  align-items: center;
  gap: 16px;
  width: min(100%, 420px);
  padding: 16px;
}

.ui-streak-body {
  flex: 1;
  min-width: 0;
}

.ui-streak-title {
  font-family: var(--ui-title);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.3;
  text-wrap: balance;
}

.ui-streak-title em {
  font-style: normal;
  color: var(--ui-orange);
}

.ui-streak-sub {
  margin-top: 4px;
  color: var(--ui-muted);
  font-family: var(--ui-title);
  font-size: 14px;
  font-weight: 600;
}

.ui-days {
  display: flex;
  justify-content: space-between;
  gap: 2px;
  margin-top: 8px;
}

.ui-days li {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--ui-variant);
  color: var(--ui-muted);
  font-size: 10px;
  font-weight: 500;
  line-height: 1;
}

.ui-days .is-done {
  background: url("../appui/day-done.svg") center / 24px 24px no-repeat;
  color: #fff;
}

.ui-days .is-today {
  background: var(--ui-blue);
  color: #fff;
}

.ui-streak-flame {
  flex: none;
  width: 72px;
  height: 72px;
}

/* ---------- Stats pair (stats_grid.dart, sessions_card.dart) ---------- */
/* At the app's own size on the 6.9" iPhone of the App Store captures (a
   188px half of the home column), so the charts draw 1:1 and their SVG never
   stretches. The band exists only during
   an event (the x2 bonus, a defi); the card keeps one height either way. */
.ui-stat {
  display: flex;
  flex-direction: column;
  width: 188px;
  padding: 4px 5px;
  border: 0.3px solid #e5e5e5;
  border-radius: 15px;
  background: #fff;
  box-shadow: var(--ui-lift);
}

.ui-band {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 60px;
  margin-bottom: 8px;
  padding: 10px 15px;
  border-radius: 13px;
  color: #fff;
}

.ui-band-orange {
  background: var(--ui-orange);
}

.ui-band-blue {
  background: var(--ui-blue);
}

.ui-band-text {
  display: grid;
  flex: 1;
  min-width: 0;
  line-height: 1.1;
}

.ui-points .ui-band-text span {
  font-size: 15px;
}

.ui-points .ui-band-text b {
  font-size: 20px;
  font-weight: 400;
}

.ui-band-icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 10px;
  background: rgb(255 255 255 / 0.22);
}

.ui-band-icon svg {
  width: 26px;
  height: 26px;
  fill: #fff;
}

/* flame.svg, tinted white as the app does (ColorFilter srcIn). */
.ui-flame-white {
  width: 24px;
  height: 24px;
  background: #fff;
  -webkit-mask: url("../appui/flame.svg") center / contain no-repeat;
  mask: url("../appui/flame.svg") center / contain no-repeat;
}

.ui-sessions .ui-band-text {
  font-family: var(--ui-title);
}

.ui-defi-from {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 800;
}

.ui-defi-badge {
  padding: 2px 6px;
  border-radius: 6px;
  background: #fff;
  color: var(--ui-blue);
  font-size: 9px;
  font-weight: 900;
}

.ui-defi-count {
  font-size: 20px;
  font-weight: 900;
}

.ui-chart {
  width: 178px;
  height: 60px;
  overflow: visible;
}

.ui-chart-line {
  fill: none;
  stroke: var(--ui-orange);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ui-chart-dot {
  fill: var(--ui-orange);
}

.ui-bars {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  width: 178px;
  height: 60px;
}

.ui-bars li {
  flex: 1;
  height: var(--h, 16%);
  border-radius: 4px;
  background: var(--ui-blue-empty);
  transform-origin: 50% 100%;
}

.ui-bars .is-done {
  background: var(--ui-blue);
}

.ui-stat-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 28px;
  margin-top: 4px;
  padding: 2px 6px;
  line-height: 1.2;
}

.ui-stat-foot b {
  font-size: 20px;
  font-weight: 800;
}

.ui-stat-foot span {
  font-size: 10px;
  font-weight: 600;
}

.ui-points .ui-stat-foot {
  color: var(--ui-orange);
}

.ui-points .ui-stat-foot b,
.ui-points .ui-stat-foot span {
  font-weight: 400;
}

.ui-sessions .ui-stat-foot {
  color: var(--ui-blue-ink);
  font-family: var(--ui-title);
}

/* ---------- Leaderboard (lib/screens/home/widgets/leaderboard_card.dart) ---------- */
/* Live: site.js re-sorts the rows as members finish sessions (FLIP). The
   rank is a medal for the podium (amber, grey, bronze), a figure below. */
.ui-board {
  width: min(100%, 390px);
  padding: 16px 16px 8px;
}

.ui-row {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
  padding: 8px;
  border-radius: 12px;
}

.ui-row.is-me {
  background: rgb(243 115 10 / 0.12);
}

.ui-rank {
  display: grid;
  flex: none;
  place-items: center;
  width: 22px;
  margin-right: 8px;
}

.ui-rank svg {
  width: 20px;
  height: 20px;
  fill: #bdbdbd;
}

.ui-row[data-place="1"] .ui-rank svg {
  width: 22px;
  height: 22px;
  fill: #ffc107;
}

.ui-row[data-place="3"] .ui-rank svg {
  fill: #d97706;
}

.ui-rank-num {
  display: none;
  color: var(--ui-orange);
  font-size: 14px;
  font-weight: 500;
}

.ui-row:not([data-place="1"], [data-place="2"], [data-place="3"]) .ui-rank svg {
  display: none;
}

.ui-row:not([data-place="1"], [data-place="2"], [data-place="3"]) .ui-rank-num {
  display: block;
}

.ui-name {
  flex: 1;
  min-width: 0;
  margin-left: 12px;
  overflow: hidden;
  font-size: 16px;
  line-height: 1.5;
  text-overflow: ellipsis;
}

.ui-row.is-me .ui-name {
  color: var(--ui-orange);
}

.ui-pts {
  font-size: 14px;
  font-weight: 500;
}

.ui-row[data-moved] .ui-pts {
  animation: ptsPulse 0.7s var(--ease);
}

@keyframes ptsPulse {
  40% {
    scale: 1.18;
  }
}

.ui-more {
  padding-block: 4px 8px;
  color: var(--ui-orange);
  font-size: 12px;
  font-weight: 500;
  text-align: center;
}

/* ---------- Group banner + weekly goal (group_banner.dart, weekly_goal_card.dart) ---------- */
.ui-group {
  width: min(100%, 400px);
}

.ui-banner {
  position: relative;
  z-index: 1;
  margin: 0;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 20px;
  /* The app's shadowMd: the depth comes from the card under it. */
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
}

.ui-banner img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ui-banner::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 42%, rgb(0 0 0 / 0.6));
}

.ui-banner-name {
  position: absolute;
  z-index: 1;
  right: 16px;
  bottom: 16px;
  left: 20px;
  color: #fff;
  font-family: var(--ui-title);
  font-size: 28px;
  font-weight: 800;
  line-height: 1.2;
  text-shadow: 0 2px 14px rgb(0 0 0 / 0.55);
}

/* The card slides out from under the banner: 18px of overlap paid back in
   padding, 20px radius, 18px sides. */
.ui-goal {
  margin-top: -18px;
  padding: 34px 18px 16px;
  border-radius: 20px;
}

.ui-goal-count {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-family: var(--ui-title);
  line-height: 1;
}

.ui-goal-count b {
  font-size: 40px;
  font-weight: 800;
}

.ui-goal-count span {
  color: var(--ui-muted);
  font-size: 20px;
  font-weight: 700;
}

/* One segment per member who trained (member colour, length = sessions),
   then what is left, 3px gutters. site.js sets --n; a zero segment hands
   its gutter back. The one layout transition on the site, on a 13px bar. */
.ui-goal-bar {
  display: flex;
  gap: 3px;
  height: 13px;
  margin-top: 12px;
}

.ui-goal-bar i {
  flex: var(--n, 0) 1 0%;
  min-width: 0;
  border-radius: 999px;
  background: var(--c, #e4e6ea);
  transition: flex-grow 0.6s var(--ease), margin 0.6s var(--ease);
}

.ui-goal-bar i[data-empty] {
  margin-inline-start: -3px;
}

.ui-goal-members {
  display: flex;
  gap: 7px;
  height: 32px;
  margin-top: 8px;
}

.ui-goal-members li {
  display: flex;
  flex: 1;
  align-items: center;
  gap: 5px;
  min-width: 0;
  padding: 4px 8px 4px 3px;
  border-radius: 999px;
  background: var(--ui-variant);
}

.ui-goal-members .ui-ava {
  width: 22px;
  height: 22px;
}

.ui-goal-members span.ui-ava {
  font-size: 8.8px;
}

/* Name and count split what is left of the chip (Expanded + Flexible in the
   app): the figure sits mid-chip, not at its end. */
.ui-chip-name {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  color: var(--ui-medium);
  font-family: var(--ui-title);
  font-size: 11.5px;
  font-weight: 600;
  text-overflow: ellipsis;
}

.ui-chip-count {
  flex: 1 1 0;
  min-width: 0;
  color: var(--ui-green);
  font-family: var(--ui-title);
  font-size: 12px;
  font-weight: 800;
}

.ui-goal-members li[data-moved] .ui-chip-count {
  animation: ptsPulse 0.7s var(--ease);
}

/* ---------- Group chat (group_screen.dart: _messagesCard, _messageRow) ---------- */
.ui-chat {
  width: min(100%, 400px);
  padding: 12px 14px;
}

.ui-msg {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.ui-msg + .ui-msg {
  margin-top: 9.5px;
  padding-top: 9.5px;
  border-top: 1px solid var(--ui-variant);
}

.ui-msg .ui-ava {
  width: 38px;
  height: 38px;
}

.ui-msg span.ui-ava {
  font-size: 15.2px;
}

.ui-msg-body {
  flex: 1;
  min-width: 0;
}

.ui-msg-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.ui-msg-head b {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.5;
}

.ui-msg-head span {
  color: var(--ui-light);
  font-size: 12px;
}

/* Two lines at most, as in the app. */
.ui-msg-text {
  display: -webkit-box;
  margin-top: 3px;
  overflow: hidden;
  color: var(--ui-medium);
  font-size: 14px;
  line-height: 1.5;
  text-wrap: pretty;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.ui-composer {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}

.ui-input {
  flex: 1;
  min-width: 0;
  padding: 13px 16px;
  overflow: hidden;
  border: 1px solid var(--ui-border);
  border-radius: 26px;
  color: var(--ui-light);
  font-size: 14px;
  line-height: 1.5;
  text-overflow: ellipsis;
}

.ui-send {
  display: grid;
  flex: none;
  place-items: center;
  width: 48px;
  height: 48px;
  border-radius: 14px;
  background: var(--ui-orange-soft);
}

.ui-send svg {
  width: 22px;
  height: 22px;
  fill: var(--ui-orange);
}

/* ---------- Entrances ---------- */
/* Each widget plays once, when its tile is revealed (.is-in), the way the
   app draws it: days light up, the curve draws, the bars rise, the rows and
   the messages land. Reduced motion: the finished widget, still. */
@media (prefers-reduced-motion: no-preference) {
  .is-in .ui-days li {
    animation: uiPop 0.45s var(--ease) both;
    animation-delay: calc(var(--d, 0) * 80ms + 250ms);
  }

  .is-in .ui-streak-flame {
    animation: uiPop 0.7s var(--ease) 0.2s both;
  }

  .ui-chart-line {
    stroke-dasharray: 1;
  }

  .is-in .ui-chart-line {
    animation: uiDraw 1.1s var(--ease) 0.25s both;
  }

  .is-in .ui-chart-fill {
    animation: uiFade 0.7s var(--ease) 0.85s both;
  }

  .is-in .ui-chart-dot {
    animation: uiPop 0.45s var(--ease) 1.1s both;
    transform-box: fill-box;
    transform-origin: center;
  }

  .is-in .ui-bars li {
    animation: uiGrow 0.55s var(--ease) both;
    animation-delay: calc(var(--i, 0) * 60ms + 250ms);
  }

  .is-in .ui-row {
    animation: uiRise 0.5s var(--ease) both;
    animation-delay: calc(var(--r, 0) * 80ms + 150ms);
  }

  .is-in .ui-msg {
    animation: uiRise 0.55s var(--ease) both;
    animation-delay: calc(var(--m, 0) * 140ms + 200ms);
  }

  @keyframes uiDraw {
    from {
      stroke-dashoffset: 1;
    }
    to {
      stroke-dashoffset: 0;
    }
  }

  @keyframes uiFade {
    from {
      opacity: 0;
    }
  }
}

/* =========================================================================
   Sync: the apps Tribos will read from (planned)

   A hub, not a logo wall: Tribos in the centre, the other apps around it on
   two rings, a thread from each to the centre. Names, never logos: the
   official badges ("Works with Apple Health", "Compatible with Strava") may
   only appear once an integration ships, and other marks get the same
   treatment. The block says "Bientôt" (.soon) until the first one does.
   ========================================================================= */
.sync {
  display: grid;
  gap: clamp(2.5rem, 1rem + 4vw, 5rem);
  align-items: center;
}

@media (min-width: 1000px) {
  .sync {
    grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
  }
}

.sync-copy {
  display: grid;
  gap: var(--s-4);
  justify-items: start;
  max-width: 48ch;
}

.sync-note {
  color: var(--text-muted);
  font-size: var(--step--1);
}

/* Status pill: a planned feature, said once and plainly. A hollow dot, where
   the live eyebrow has a filled, pulsing one. */
.soon {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.32rem 0.85rem 0.32rem 0.7rem;
  border: 1px solid rgb(255 162 92 / 0.42);
  border-radius: 999px;
  background: rgb(243 115 10 / 0.1);
  color: var(--accent-text);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.1em;
  line-height: 1.4;
  text-transform: uppercase;
}

.soon::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
}

.sync-hub {
  position: relative;
  width: 100%;
  max-width: 660px;
  margin-inline: auto;
  aspect-ratio: 1.12;
}

/* The two rings the names sit on. */
.sync-hub::before,
.sync-hub::after {
  content: "";
  position: absolute;
  border: 1px dashed rgb(255 236 214 / 0.14);
  border-radius: 50%;
  pointer-events: none;
}

.sync-hub::before {
  inset: 24% 20%;
}

.sync-hub::after {
  inset: 7% 10%;
}

.sync-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.sync-lines line {
  stroke: rgb(255 162 92 / 0.34);
  stroke-width: 1.25;
  stroke-dasharray: 3 5;
  vector-effect: non-scaling-stroke;
}

.sync-core {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  width: clamp(84px, 16%, 108px);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 24%;
  box-shadow:
    0 0 0 10px rgb(243 115 10 / 0.1),
    0 0 70px rgb(243 115 10 / 0.42),
    var(--shadow-2);
}

.sync-core img {
  width: 100%;
  height: 100%;
  border-radius: inherit;
}

.sync-apps li {
  position: absolute;
  top: var(--y);
  left: var(--x);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.95rem 0.5rem 0.8rem;
  translate: -50% -50%;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--shadow-1);
  color: var(--text);
  font-size: 0.92rem;
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
}

.sync-apps li::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-text);
  opacity: 0.7;
}

.sync-legal {
  margin-top: var(--s-6);
  max-width: 72ch;
  color: var(--text-muted);
  font-size: 0.8rem;
}

/* A phone has no room for rings: the names wrap under the icon. */
@media (max-width: 699px) {
  .sync-hub {
    display: grid;
    justify-items: center;
    gap: var(--s-5);
    aspect-ratio: auto;
  }

  .sync-hub::before,
  .sync-hub::after,
  .sync-lines {
    display: none;
  }

  .sync-core {
    position: relative;
    top: auto;
    left: auto;
    width: 92px;
    translate: none;
  }

  .sync-apps {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.55rem;
  }

  .sync-apps li {
    position: static;
    translate: none;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .is-in .sync-apps li {
    animation: uiPop 0.5s var(--ease) both;
    animation-delay: calc(var(--k, 0) * 70ms + 300ms);
  }

  .is-in .sync-lines line {
    animation: uiFade 0.9s var(--ease) both;
    animation-delay: calc(var(--k, 0) * 70ms + 450ms);
  }

  .is-in .sync-core {
    animation: uiPop 0.7s var(--ease) 0.1s both;
  }
}

/* =========================================================================
   Partner gyms (slots)

   The room is ready before the guests: empty logo slots under a short
   invitation. Nothing claims a partnership that does not exist. When one is
   signed, its logo takes a slot (.partner-slot > img, monochrome ivory, about
   40px high) and the empty slots that remain keep the grid whole.
   ========================================================================= */
.partners {
  display: grid;
  gap: clamp(2rem, 1rem + 3vw, 4rem);
  align-items: center;
  padding: clamp(1.75rem, 1rem + 3vw, 3.5rem);
  border: 1px solid var(--line);
  border-radius: var(--r-media);
  background:
    radial-gradient(60% 90% at 0% 100%, rgb(243 115 10 / 0.12), transparent 70%),
    var(--bg-2);
}

@media (min-width: 900px) {
  .partners {
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  }
}

.partners-copy {
  display: grid;
  gap: var(--s-4);
  justify-items: start;
}

.partners-copy .btn {
  margin-top: var(--s-2);
}

.partner-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-top: 1px solid var(--line);
  border-left: 1px solid var(--line);
}

.partner-slot {
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 10;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.partner-slot img {
  width: auto;
  max-width: 70%;
  max-height: 40px;
}

/* An empty slot: a dashed socket waiting for a logo. The first one says what
   the sockets are for; the others carry a plus. */
.partner-slot.is-empty:empty::before,
.partner-slot.is-empty > span {
  display: grid;
  place-items: center;
  width: 62%;
  height: 48%;
  border: 1px dashed rgb(255 236 214 / 0.2);
  border-radius: 12px;
}

.partner-slot.is-empty:empty::before {
  content: "+";
  color: rgb(255 236 214 / 0.34);
  font-size: 1.3rem;
  font-weight: 300;
  line-height: 1;
}

.partner-slot.is-empty > span {
  padding-inline: 0.4rem;
  color: var(--text-muted);
  font-size: clamp(0.68rem, 0.62rem + 0.3vw, 0.8rem);
  font-weight: 500;
  line-height: 1.2;
  text-align: center;
}

/* =========================================================================
   Motion layer

   Content reveal is never scroll-driven: anything that decides whether text
   is visible runs on the IntersectionObserver path (.reveal -> .is-in), gated
   on `.js`, which the head script sets before first paint and drops after
   2.5s if site.js never runs.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {
  .js .reveal {
    opacity: 0;
    translate: 0 22px;
    transition: opacity 0.8s var(--ease), translate 0.8s var(--ease);
    transition-delay: calc(var(--i, 0) * 80ms);
  }

  .js .reveal.is-in {
    opacity: 1;
    translate: 0 0;
  }

  .js [data-anim="slide"] {
    opacity: 0;
    translate: -34px 0;
    transition: opacity 0.7s var(--ease), translate 0.7s var(--ease);
    transition-delay: calc(var(--i, 0) * 70ms);
  }

  .js [data-anim="pop"] {
    opacity: 0;
    scale: 0.95;
    transition: opacity 0.6s var(--ease), scale 0.6s var(--ease);
    transition-delay: calc(var(--i, 0) * 70ms);
  }

  .js [data-anim].is-in {
    opacity: 1;
    scale: 1;
    translate: 0 0;
  }
}

/* Per-word headline entrance. A short rise plus a fade, never the overflow
   mask: an inline-block with overflow hidden grows every line box. */
.word,
.word-in {
  display: inline-block;
}

@media (prefers-reduced-motion: no-preference) {
  .js [data-split] .word-in {
    opacity: 0;
    translate: 0 calc(16 * var(--lift));
    transition: opacity 0.7s var(--ease), translate 0.8s var(--ease);
    transition-delay: calc(var(--w, 0) * var(--split-step, 46ms) + var(--split-delay, 0ms));
  }

  .js [data-split].is-in .word-in {
    opacity: 1;
    translate: 0 0;
  }
}

/* The hero copy waits for the first beat of the push-in. */
.hero h1 {
  --split-delay: 260ms;
}

/* Children index themselves so markup stops carrying style="--i: N". */
.bento .tile:nth-child(1) { --i: 0; }
.bento .tile:nth-child(2) { --i: 1; }
.bento .tile:nth-child(3) { --i: 2; }
.bento .tile:nth-child(4) { --i: 3; }
.bento .tile:nth-child(5) { --i: 4; }
.bento .tile:nth-child(6) { --i: 5; }

/* ---------- Points burst ---------- */
.burst {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}

.burst i {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-text);
  opacity: 0;
  animation: burstOut 0.72s var(--ease) forwards;
  animation-delay: var(--d, 0ms);
}

@keyframes burstOut {
  0% {
    opacity: 1;
    translate: -50% -50%;
    scale: 0.4;
  }
  100% {
    opacity: 0;
    translate: calc(-50% + var(--dx)) calc(-50% + var(--dy));
    scale: 1;
  }
}

/* ---------- Magnetic + tilt hooks ---------- */
/* site.js only writes --mx/--my and --rx/--ry; the transforms live here. */
[data-magnetic] {
  translate: var(--mx, 0) var(--my, 0);
  transition: translate 0.32s var(--ease), box-shadow 0.25s var(--ease), background-color 0.2s var(--ease);
}

[data-magnetic]:hover {
  transition-duration: 0.08s, 0.25s, 0.2s;
}

[data-tilt] {
  transform-style: preserve-3d;
  transform: perspective(1200px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform 0.5s var(--ease);
}

[data-tilt][data-tilting="true"] {
  transition-duration: 0.12s;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion: 0;
  }

  html {
    scroll-behavior: auto;
  }

  [data-magnetic],
  [data-tilt] {
    translate: none;
    transform: none;
  }

  .burst {
    display: none;
  }

  /* 1ms, not 0: sub-millisecond durations round to zero in some engines and
     transitionend never fires. Every @keyframes block in this file ends on the
     resting, legible state, so a 1ms run lands on the right frame. */
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }
}

:root[data-motion="off"] {
  --motion: 0;
}
