/* Landing + SEO pages. Everything here sits on top of theme.css tokens. */

/* ── Hero ─────────────────────────────────────────────────────────────────── */
.hero {
  position: relative;
  /* Tight on purpose. The bar is the only thing that matters above the fold,
     so every pixel of padding above it pushes the pictures off the screen. */
  padding: clamp(34px, 6vh, 62px) 0 clamp(26px, 4vh, 40px);
  overflow: hidden;
  border-bottom: 1px solid var(--line-soft);
}
/* A slow vertical light behind the text — the "something is running back
   there" feeling. It never moves fast enough to be noticed as an animation. */
.hero::before {
  content: "";
  position: absolute; inset: -30% -10% auto -10%; height: 140%;
  background: radial-gradient(58% 44% at 50% 26%, color-mix(in oklab, var(--accent) 13%, transparent), transparent 72%);
  pointer-events: none;
  animation: breathe 22s ease-in-out infinite alternate;
}
@keyframes breathe { to { transform: translateY(4%) scale(1.06); opacity: .72; } }

#dust { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; opacity: .5; }
[data-theme="light"] #dust { opacity: .28; }

/* The hero sits in the same left gutter as every section below it — a headline
   that centres itself while the rest of the page is left-aligned reads as two
   different pages stitched together. Only the text width is capped. */
.hero-in { position: relative; z-index: 1; }
.hero-in > * { max-width: 780px; }
.kicker {
  font: 500 11.5px/1 var(--mono); letter-spacing: .16em; text-transform: uppercase;
  color: var(--accent); margin-bottom: 22px;
}
.hero h1 em { font-style: normal; color: var(--accent-hi); }
.lede { font-size: clamp(15px, 1.7vw, 18.5px); max-width: 62ch; color: var(--fg-dim); }
.lede strong { color: var(--fg); font-weight: 600; }
.hero-cta { margin: 26px 0 12px; }
.btn.big { padding: 13px 24px; min-height: 46px; font-size: 15px; }

/* ── The result, in place ─────────────────────────────────────────────────── */
/* No navigation between typing and seeing. The picture opens under the bar and
   pushes the examples down — the page becomes yours as soon as you use it. */
.cmp-stage { max-width: 880px; margin-top: 16px; }
/* The frame hugs the picture instead of the picture sitting in a frame: a
   square result in an 880-wide box would otherwise come with two dead bars,
   and nothing says "template" louder than letterboxing your own output. */
.cmp-shots { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.cmp-shot {
  position: relative; margin: 0; flex: 0 1 auto; max-width: 100%;
  border: 1px solid var(--line); border-radius: 12px;
  overflow: hidden; background: var(--bg-1);
  animation: shotIn 480ms var(--ease) both;
}
@keyframes shotIn { from { opacity: 0; transform: translateY(10px); clip-path: inset(0 0 100% 0); } }
/* The figure carries the cap, not the image: constraining an image by height
   leaves its parent at the picture's full intrinsic width, and the frame ends
   up floating well to the right of what it frames. The composer writes
   max-width: min(100%, 62vh × ratio) inline from the real pixel size, so the
   border lands exactly on the edge whatever shape comes back. */
.cmp-shot img { display: block; width: 100%; height: auto; background: var(--bg-0); }
.cmp-shot .bar {
  display: flex; gap: 8px; align-items: center; padding: 9px 10px;
  border-top: 1px solid var(--line-soft);
}
.cmp-shot .bar .spacer { flex: 1; }
.cmp-wait {
  display: grid; place-items: center; gap: 10px; min-height: 200px;
  border: 1px solid var(--line-soft); border-radius: 12px;
  background: linear-gradient(100deg, var(--bg-1) 30%, var(--bg-2) 50%, var(--bg-1) 70%) 0 0 / 300% 100%;
  animation: sweep 2.6s linear infinite;
  color: var(--fg-faint); font: 500 12px var(--mono); letter-spacing: .08em; text-transform: uppercase;
}
@keyframes sweep { to { background-position: -300% 0; } }

@media (prefers-reduced-motion: reduce) {
  .cmp-shot { animation: none; }
  .cmp-wait { animation: none; }
}

/* ── Sections ─────────────────────────────────────────────────────────────── */
.section { padding: clamp(36px, 5vh, 56px) 0; border-bottom: 1px solid var(--line-soft); }
.section-lede { max-width: 60ch; margin-bottom: 20px; }
/* A section with no heading of its own — the content IS the heading. Without
   this it inherits the padding meant to sit under a title and opens on a
   band of nothing. */
.section-bare { padding-top: clamp(22px, 3vh, 32px); }

/* ── Use-case grid ────────────────────────────────────────────────────────── */
.grid-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(258px, 1fr)); gap: 12px; }
.uc {
  position: relative; display: block; padding: 16px 17px 18px;
  background: var(--bg-1); border: 1px solid var(--line-soft); border-radius: var(--radius-lg);
  transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease), background var(--dur) var(--ease);
}
/* The left rule fills from the top on hover — a small vertical gesture that
   says "this is a way in", instead of a colour change that says nothing. */
.uc::before {
  content: ""; position: absolute; left: -1px; top: -1px; bottom: -1px; width: 2px;
  background: var(--accent); transform: scaleY(0); transform-origin: top;
  transition: transform 340ms var(--ease);
  border-radius: var(--radius-lg) 0 0 var(--radius-lg);
}
.uc:hover { border-color: var(--line); background: var(--bg-2); }
.uc:hover::before { transform: scaleY(1); }
/* The gutter is the arrow's, not the title's: "AI Outfit Generator & Virtual
   Try-On" wrapped straight under the → and the two overlapped. */
.uc h3 { font-size: 15.5px; margin-bottom: 5px; padding-right: 30px; }
.uc p { font-size: 13px; margin: 0; color: var(--fg-faint); line-height: 1.45; }
.uc .go { position: absolute; right: 14px; top: 15px; color: var(--fg-faint); transition: transform var(--dur) var(--ease), color var(--dur) var(--ease); }
.uc:hover .go { color: var(--accent); transform: translateX(3px); }

/* Families. Twenty-one cards in one uninterrupted grid is a list nobody reads
   to the end — and to a crawler it is twenty-one links with no relation
   between them. The headings say what each run of cards is FOR, which is the
   same thing in both languages. The rule that runs off to the right is the
   archive divider, not decoration: it is what tells the eye a new run starts
   without a box or a colour. */
.uc-fams { display: grid; gap: 26px; }
.uc-fam > h3 {
  display: flex; align-items: center; gap: 12px; margin: 0 0 11px;
  font-size: 11.5px; font-weight: 600; letter-spacing: .13em; text-transform: uppercase;
  color: var(--fg-faint);
}
/* It fades out instead of crossing the whole width: at full strength the rule
   reads as a table border and cuts the grid in two. Strong where it leaves the
   label, gone by the far edge — --line-soft flat was invisible in the dark
   theme and a solid --line was a wall in the light one. */
.uc-fam > h3::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, var(--line), transparent 78%);
}

/* ── Steps ────────────────────────────────────────────────────────────────── */
.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 18px; max-width: 760px; }
.steps li { display: flex; gap: 16px; align-items: flex-start; }
.steps .n {
  flex: none; width: 30px; height: 30px; display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 50%;
  font: 600 13px/1 var(--mono); color: var(--accent); background: var(--bg-1);
}
.steps h3 { font-size: 16px; margin-bottom: 3px; }
.steps p { margin: 0; font-size: 14px; }
.steps.compact { gap: 12px; }

/* The free trial, said once in static markup. Left rule in the accent rather
   than a filled badge: this sits directly above the pack cards, which already
   carry an accent ribbon, and two accent blocks stacked read as an advert. */
.free-line {
  display: flex; align-items: baseline; gap: 12px;
  margin: -4px 0 20px; padding-left: 14px;
  border-left: 2px solid var(--accent);
  max-width: 62ch; font-size: 15px; color: var(--fg-dim);
}
.free-line .free-n {
  flex: none;
  font: 700 30px/1 var(--display); color: var(--accent);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
}
.free-line b { color: var(--fg); font-weight: 600; }
/* DAY (theme.css §DAY): on the acid field every rule is ink and nothing else —
   a coloured rule would be the fourth device the poster refuses. The number
   keeps the accent, because accent-as-TEXT is allowed there and reads 4.7:1. */
[data-theme="light"] .free-line { border-left-color: var(--line); }

/* ── Packs ────────────────────────────────────────────────────────────────── */
/* 1010 px et pas 900 : depuis le barreau à 0 € la grille compte SIX cartes, et
   6×150 + 5×10 = 950 — la sixième (Premium, celle qui rapporte) tombait seule
   sur une deuxième ligne. La largeur suit le nombre de cartes, pas l'inverse. */
.packs { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; max-width: 1010px; }
.pack {
  position: relative; display: flex; flex-direction: column; align-items: flex-start;
  padding: 22px 16px 17px; text-decoration: none; color: var(--fg);
  background: var(--bg-1); border: 1px solid var(--line-soft); border-radius: var(--radius-lg);
  transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease),
              background var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.pack:hover {
  border-color: var(--accent); background: var(--bg-2);
  transform: translateY(-3px); box-shadow: 0 10px 26px -18px var(--accent);
}
.pack.best { border-color: color-mix(in oklab, var(--accent) 55%, var(--line)); background: var(--bg-2); }
.pack .pk-ribbon {
  position: absolute; top: -1px; right: -1px;
  font: 600 9px/1 var(--display); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--bg-0); background: var(--accent); padding: 5px 8px;
  border-radius: 0 var(--radius-lg) 0 var(--radius);
}
/* Same three parts as the wallet card (css/app.css): the tier NAME above the
   number, and the lane ceiling drawn as ticks under the rate. Both are served
   from PACKS/LIMITS — this page never types a promise of its own. */
.pack .pname { font: 600 9.5px/1 var(--display); letter-spacing: 0.18em; text-transform: uppercase; color: var(--fg-faint); margin-bottom: 9px; }
.pack.pro .pname { color: var(--accent); }
.pack .amt { font: 600 clamp(28px,3.2vw,36px)/1 var(--display); letter-spacing: -0.015em; font-variant-numeric: tabular-nums; }
.pack .unit { margin-top: 5px; font: 500 10px/1 var(--display); letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-faint); }
.pack .free { margin-top: 10px; min-height: 17px; font: 600 12px/1.2 var(--sans); color: var(--accent); }
.pack .rule { display: block; width: 26px; height: 1px; background: var(--line); margin: 12px 0 10px; }
.pack .pay { font: 600 16px/1 var(--sans); }
.pack .rate { margin-top: 4px; font: 500 11px/1.3 var(--mono); color: var(--fg-dim); }
.pack .rate s { opacity: 0.5; margin-right: 2px; }
.pack .perk { display: flex; align-items: center; gap: 7px; margin-top: 10px; font: 500 10.5px/1 var(--mono); color: var(--fg-faint); }
.pack .perk::before {
  content: ''; flex: none; height: 9px;
  width: calc(var(--lanes) * 7px - 4px);
  background: repeating-linear-gradient(90deg, currentColor 0 3px, transparent 3px 7px);
  opacity: 0.75;
}
.pack .perk.on { color: var(--accent); }
.pack .use { margin-top: 10px; font-size: 11.5px; line-height: 1.35; color: var(--fg-faint); }
/* ── L'EXTRA COMPANION — UNE ADDITION, PAS UNE LIGNE DE PLUS ─────────────────
   Bloc VOLONTAIREMENT IDENTIQUE dans app.css et landing.css : la carte de pack
   a trois portes (page de prix, onglet Portefeuille, panneau « Add credit ») et
   son markup vient d'une seule fonction (js/ui.js : companionAddon). Deux
   copies du style, c'est deux versions qui divergent — le bug que Phil vient de
   pointer (la page de prix avait la ligne rose, le portefeuille non).
   Le + est gros et tenu par deux filets : il dit « et EN PLUS » avant qu'un mot
   soit lu. Rose companion (#ff6fa8), jamais le jaune de la maison : la ligne
   doit se lire comme venant d'AILLEURS, donc d'un autre compteur. */
/* ── LE BARREAU À 0 € ────────────────────────────────────────────────────────
   Même carte que les autres, volontairement : une offre gratuite dessinée
   autrement se lit comme une note de bas de page. Elle est juste plus sobre —
   pas de jaune, pas de relief — parce qu'elle n'est pas ce qu'on vend. */
.pk-zero { border-style: dashed; text-decoration: none; }
.pk-zero .pk-n { color: var(--fg-dim); }
.pk-zero .pk-pay { color: var(--fg-dim); }
.pk-grid .pk-zero { cursor: default; }
.pk-grid .pk-zero:hover { transform: none; box-shadow: none; border-color: var(--line-soft); background: var(--bg-2); }

.pk-plus {
  display: flex; align-items: center; gap: 9px;
  width: 100%; margin-top: auto; padding-top: 13px;
}
.pk-plus::before, .pk-plus::after {
  content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(201, 79, 124, .5));
}
.pk-plus::after { background: linear-gradient(270deg, transparent, rgba(201, 79, 124, .5)); }
.pk-plus i {
  flex: none; font: 600 24px/1 var(--display); font-style: normal;
  color: #ff6fa8; translate: 0 -1px;
}
.pk-companion {
  display: block; width: 100%; margin-top: 8px; padding: 8px 9px;
  border: 1px solid rgba(201, 79, 124, .55); border-radius: 10px;
  background: rgba(201, 79, 124, .10);
  font: 500 10.5px/1.35 var(--mono); color: #e693b4; letter-spacing: .01em;
}
.pk-companion b {
  display: block; margin-bottom: 4px;
  font: 600 9.5px/1 var(--display); letter-spacing: .16em; text-transform: uppercase;
  color: #ff6fa8;
}
/* Le nombre sur SA ligne : cinq nombres l'un sous l'autre se comparent d'un
   coup d'œil, « 120 messages a / day » qui passe à la ligne se subit. Même
   ordre que la carte au-dessus (le nombre, puis l'unité), pour que les deux
   monnaies se lisent de la même façon. */
.pk-companion em {
  display: block; margin-top: 2px;
  font: 600 20px/1 var(--display); font-style: normal;
  color: #ffd0e2; font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.pk-companion u {
  display: block; margin-top: 3px; text-decoration: none;
  font: 500 9px/1 var(--display); letter-spacing: .14em; text-transform: uppercase;
  color: rgba(230, 147, 180, .8);
}
/* La deuxième ligne de l'encart (le corps 3D) se lit comme une PRÉCISION,
   pas comme un deuxième titre : même famille, un ton en dessous. */
.pk-companion u.pk-body { margin-top: 5px; letter-spacing: .1em; opacity: .8; }
.pk-grid .pk:hover .pk-companion, .pack:hover .pk-companion { border-color: rgba(201, 79, 124, .9); }

/* ── SEO page body ────────────────────────────────────────────────────────── */
.seo-page { max-width: 760px; padding: 46px 0 70px; }
.seo-page h2 { margin-top: 42px; }
.seo-page .lede { font-size: 17px; color: var(--fg); }
/* ── The trust strip ────────────────────────────────────────────────────────
   Four promises under the CTA on every landing page (render() in
   src/seo-pages.js). Deliberately NOT a card row: cards read as marketing and
   get skipped, a tight list right under the button reads as terms and gets
   read. Auto-fit rather than a fixed column count so it is two lines on a
   laptop, one column on a phone, and never a widowed fourth item. */
.trust {
  list-style: none; padding: 0; margin: 0 0 34px;
  display: grid; gap: 6px 22px; max-width: 760px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.trust li {
  position: relative; padding-left: 16px;
  font-size: 13.5px; line-height: 1.55; color: var(--fg-dim);
}
/* The tick is a pseudo-element, not a character in the markup: it must not end
   up in the text a screen reader (or a snippet generator) reads back. */
.trust li::before {
  content: ''; position: absolute; left: 0; top: .62em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); opacity: .75;
}
.trust b { color: var(--fg); font-weight: 600; }

.prompt-list { list-style: none; padding: 0; display: grid; gap: 8px; }
.prompt-list li { background: var(--bg-1); border: 1px solid var(--line-soft); border-left: 2px solid var(--accent); border-radius: var(--radius); padding: 10px 13px; }
.prompt-list code { color: var(--fg-dim); font-size: 13px; line-height: 1.5; }
.faq dt { font-weight: 600; margin-top: 18px; }
.faq dd { margin: 5px 0 0; color: var(--fg-dim); }
.related { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.related a {
  display: inline-block; padding: 7px 13px; font-size: 13.5px;
  border: 1px solid var(--line-soft); border-radius: 20px; color: var(--fg-dim);
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.related a:hover { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); }

/* ── Footer ───────────────────────────────────────────────────────────────── */
.foot { padding: 34px 0 46px; }
.foot a { color: var(--fg-dim); font-size: 13.5px; margin-left: 18px; }
.foot a:hover { color: var(--accent); }
.foot .tiny { margin-top: 16px; }

/* ── Showcase mosaic ─────────────────────────────────────────────────────── */
/* Dense auto-flow: tiles claim the footprint their aspect ratio deserves and
   the browser back-fills the holes. No fixed layout to maintain when a tile is
   added or dropped from the manifest. */
/* Every tile is two rows tall — only the WIDTH varies. Mixed heights plus
   dense flow leaves holes wherever a tall tile lands near the end, and a hole
   in a gallery reads as a broken image, not as a layout. Uniform bands, one
   ragged last row, no gaps. */
.mosaic {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(224px, 1fr));
  grid-auto-rows: 152px;
  grid-auto-flow: dense;
  gap: 8px;
}
.mosaic .tile { grid-row: span 2; }
.mosaic .tile.wide { grid-column: span 2; }
@media (max-width: 620px) {
  .mosaic { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 104px; }
}

.tile {
  position: relative; display: block; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-2); text-decoration: none;
  opacity: 0; transform: translateY(14px);
}
/* The stagger is positional, so the eye is led left-to-right through the row
   that just arrived rather than watching twelve things fade at once. */
.tile.in {
  opacity: 1; transform: none;
  transition: opacity 620ms var(--ease), transform 620ms var(--ease);
  transition-delay: calc(var(--i) * 55ms);
}
@media (prefers-reduced-motion: reduce) { .tile { opacity: 1; transform: none; } }

.tile-go { position: absolute; inset: 0; display: block; text-decoration: none; }
.tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: saturate(.92);
  transition: transform 900ms var(--ease), filter 500ms var(--ease);
}
.tile:hover img, .tile-go:focus-visible img { transform: scale(1.045); filter: saturate(1.04); }

/* The vote row sits ON the picture, bottom-left, and only wakes on hover —
   twelve permanent star rows would turn a gallery into a spreadsheet. */
.vote {
  position: absolute; z-index: 3; left: 7px; bottom: 7px;
  display: flex; align-items: center; gap: 6px;
  padding: 3px 8px 3px 6px; border-radius: 999px;
  background: var(--img-scrim);
  border: 1px solid rgba(255, 255, 255, .16);
  backdrop-filter: blur(6px);
  opacity: 0; transform: translateY(4px);
  transition: opacity 240ms var(--ease), transform 240ms var(--ease);
}
.tile:hover .vote, .tile:focus-within .vote, .vote.voted { opacity: 1; transform: none; }
.vote .stars { display: inline-flex; }
.vote .star {
  background: none; border: 0; padding: 0 1px; cursor: pointer;
  font-size: 12px; line-height: 1; color: var(--on-img-faint);
  transition: color 140ms var(--ease);
}
.vote .stars:hover .star { color: var(--accent); }
.vote .stars .star:hover ~ .star { color: var(--on-img-faint); }
.vote .star.on { color: var(--accent); }
.vote .score { font-size: 10px; color: var(--on-img-dim); letter-spacing: .02em; white-space: nowrap; }
/* The word box only exists after a star has been given, so it is allowed to be
   wide: at that moment the pill has the tile's attention anyway. */
.vote .say {
  width: 124px; border: 0; background: none; outline: none;
  font: inherit; font-size: 10.5px; color: var(--on-img);
  /* The underline has to stay INSIDE the pill: at the default height it landed
     exactly on the pill's own bottom border and read as a broken edge. */
  border-bottom: 1px solid rgba(255, 255, 255, .28);
  padding: 0 0 2px; margin-bottom: 2px; line-height: 1.1;
}
.vote .say::placeholder { color: var(--on-img-faint); }
.vote .say:focus { border-bottom-color: var(--accent); }

/* Anything that sits ON a picture is coloured against the PICTURE, never
   against the page. These three tokens used to read var(--bg) — a token that
   does not exist (the real ones are --bg-0…3), so the colour-mix was invalid
   and every scrim silently fell back to transparent. It went unnoticed while
   the page was dark and the ink was light; the moment a dark tile met the
   light theme, dark ink landed on a night sky and the label vanished. */
.tile {
  --img-scrim: rgba(8, 9, 12, .66);
  --on-img: #f4f2ed;
  --on-img-dim: rgba(244, 242, 237, .74);
  --on-img-faint: rgba(244, 242, 237, .45);
}
.tile .who {
  position: absolute; top: 7px; left: 7px; z-index: 2;
  font-size: 10px; letter-spacing: .05em; text-transform: uppercase;
  padding: 3px 7px; border-radius: 999px;
  color: var(--on-img); background: var(--img-scrim);
  border: 1px solid rgba(255, 255, 255, .16);
  backdrop-filter: blur(6px);
}
.tile .cap {
  position: absolute; inset: auto 0 0 0; z-index: 2;
  padding: 30px 10px 32px;
  display: grid; gap: 2px;
  /* Three lines of text now sit here, so the veil has to reach higher and start
     denser — on a bright picture (a lit garden, a white wall) the old single
     fade left the top line floating on the image, unreadable. */
  background: linear-gradient(to top,
    rgba(8, 9, 12, .94) 0%,
    rgba(8, 9, 12, .78) 55%,
    transparent 100%);
  transform: translateY(101%);
  transition: transform 320ms var(--ease);
}
.tile:hover .cap, .tile:focus-visible .cap { transform: none; }
.tile .cap b { font-size: 12.5px; font-weight: 500; color: var(--on-img); line-height: 1.25; }
.tile .cap i { font-style: normal; font-size: 10.5px; color: var(--accent); letter-spacing: .02em; }
/* The short word rides with the caption, one line, clipped rather than wrapped:
   a tile is not the place to read, it is the place to be told it worked. */
.tile .cap .word {
  quotes: none; font-size: 10.5px; color: var(--on-img-dim); font-style: italic;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.tile .cap .word:empty { display: none; }

/* ── Voices ──────────────────────────────────────────────────────────────── */
.voices {
  margin-top: 14px; min-height: 20px;
  display: flex; align-items: baseline; justify-content: center; gap: 8px;
  font-size: 12.5px; color: var(--fg-dim); text-align: center;
  opacity: 0; transform: translateY(3px);
}
.voices.in { opacity: 1; transform: none; transition: opacity 520ms var(--ease), transform 520ms var(--ease); }
.voices .v-stars { color: var(--accent); font-size: 10px; letter-spacing: .06em; }
.voices q { quotes: none; color: var(--fg); font-style: italic; }
.voices cite { font-style: normal; font-size: 11px; color: var(--fg-faint); }
@media (prefers-reduced-motion: reduce) { .voices { opacity: 1; transform: none; } }

/* ── Before / after ──────────────────────────────────────────────────────── */
/* ── The video strip ──────────────────────────────────────────────────────────
   Two vertical clips side by side, and vertical is the whole reason this is not
   the mosaic's grid: 9:16 is the shape people actually shoot and post, and it
   is the shape the tall tier sells. Capped at 340 px so two of them plus their
   captions fit one screen without scrolling — a video strip you have to scroll
   through is two videos nobody watches.

   `object-fit: contain` on black rather than `cover`: cropping a clip to fill a
   box is exactly the thing the shape picker exists to avoid, and doing it on
   the page that sells the shapes would be the site arguing with itself. */
.clip-strip { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
/* The tile is exactly as wide as the clip inside it, and that width is DERIVED
   from the clip's height rather than declared twice: 46vh at 9:16 is 25.9vh of
   width. A grid track wide enough for the caption instead of the video was the
   first attempt, and the caption then ran a hundred pixels past its own tile
   and under the neighbouring clip — one line of text, no wrapping, because
   nothing was narrow enough to make it wrap. Seen on screen, not reasoned. */
.clip { margin: 0; display: grid; gap: 10px; width: min(25.9vh, 100%); }
/* `max-height` in viewport units and not a fixed px: a 9:16 tile 340 px wide is
   604 px tall, which on a laptop pushed the section's own heading off the top of
   the screen — the strip filled the window and stopped being a strip. Measured,
   not guessed. Capped at 46vh, the heading, the lede, both clips, their captions
   and the price line all sit on one screen, which is the only arrangement in
   which a visitor sees that this is a video product at all. */
.clip video {
  /* HEIGHT drives, width follows. The other way round — width 100 % with a
     max-height — makes the BOX shorter than 9:16 while the frame inside stays
     9:16, so `contain` letterboxes it with black pillars either side: measured,
     both tiles came back with a third of their width in black. Sizing from the
     height keeps the element exactly the shape of the clip, and `max-width`
     hands control back to the column on a narrow screen. */
  height: 46vh; width: auto; max-width: 100%; aspect-ratio: 9 / 16;
  display: block; background: #000; margin-inline: auto;
  border: 1px solid var(--line-soft); border-radius: var(--radius-lg);
}
.clip figcaption { display: grid; gap: 3px; min-width: 0; }
/* `white-space: normal` because the site's `.small` sets nowrap — which is the
   right default for the one-line captions it was written for and exactly wrong
   under a 190 px tile: the sentence ran off the edge and the figure's own
   `overflow: hidden` sliced it mid-word. Two rules from two different places,
   each sensible alone. */
.clip figcaption span { display: block; white-space: normal; }
.clip figcaption b { font-size: 14px; }
/* Empty until the votes land, and `:empty` rather than a hidden attribute so
   the gap collapses too — a 3 px hole under two captions out of two is the kind
   of thing that reads as a layout bug and never as "no votes yet". */
.clip-score { font: 700 10px/1 var(--mono); letter-spacing: .1em; color: var(--fg-faint); margin-top: 3px; }
.clip-score:empty { display: none; }

/* The clip is a LINK now (js/landing.js), so the tile carries the same three
   affordances the mosaic tiles carry: something moves, something is named, and
   the frame answers. `position: relative` is what the pill hangs off. */
.clip-go { display: block; position: relative; text-decoration: none; cursor: zoom-in; }
.clip-go video { transition: border-color 260ms var(--ease), filter 400ms var(--ease); }
.clip:hover video, .clip-go:focus-visible video {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--line));
  filter: saturate(1.05) brightness(1.03);
}
/* Same gesture as a mosaic caption: it lives BELOW the frame and slides up into
   it, so the label arrives from the picture rather than fading on top of it.
   Bottom-left, inside the video's own rounded border — 1 px of inset, not 0,
   or the pill sits on the border line and reads as a torn corner. */
.clip-more {
  position: absolute; z-index: 2; left: 9px; bottom: 9px;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: 999px;
  background: rgba(8, 9, 12, .66);
  border: 1px solid rgba(255, 255, 255, .16);
  backdrop-filter: blur(6px);
  font-size: 10.5px; letter-spacing: .03em; color: #f4f2ed;
  opacity: 0; transform: translateY(8px);
  transition: opacity 240ms var(--ease), transform 240ms var(--ease);
}
.clip-more i { font-style: normal; color: var(--accent); }
.clip:hover .clip-more, .clip-go:focus-visible .clip-more { opacity: 1; transform: none; }
/* The clip you opened keeps a mark while the panel is up — same rule, same
   reason, as .tile.opened further down. */
.clip.opened .clip-go { outline: 1px solid var(--accent); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .clip-more { opacity: 1; transform: none; }
}

.ba-list { display: grid; gap: 14px; }
.ba {
  border: 1px solid var(--line-soft); border-radius: var(--radius-lg);
  background: var(--bg-1); padding: 16px 16px 14px;
  display: grid; gap: 12px;
}
.ba-head h3 { margin: 0 0 2px; font-size: 16px; font-weight: 500; }
.ba-head p { margin: 0; }

/* Tops aligned, not centres. The result is ~330 px tall and the thumbnails
   ~130, so centring left the references floating in the middle of two hundred
   pixels of nothing. Aligned to the top they share a reading line with the
   result and the empty space collects harmlessly underneath. */
.ba-body { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 16px; align-items: start; }
.ba-in { display: grid; gap: 12px; justify-items: start; align-content: start; }
/* The arrow is the one thing that must sit BETWEEN the two, so it drops to the
   middle of the thumbnail row rather than the top of the card. */
.ba-arrow { margin-top: 38px; }
@media (max-width: 780px) {
  .ba-body { grid-template-columns: 1fr; }
  .ba-arrow { transform: rotate(90deg); justify-self: center; margin-top: 0; }
  .ba-after { justify-self: center; }
}

.ba-refs { display: flex; gap: 8px; flex-wrap: wrap; }
.ba-refs figure { margin: 0; width: 104px; }
.ba-refs img {
  width: 104px; height: 104px; object-fit: cover; display: block;
  border-radius: var(--radius); border: 1px solid var(--line);
}
.ba-refs figcaption {
  display: flex; align-items: center; gap: 5px;
  margin-top: 5px; font-size: 11px; color: var(--fg-dim);
}
.ba-refs figcaption b {
  display: grid; place-items: center; width: 15px; height: 15px; flex: none;
  border-radius: 50%; background: var(--accent); color: var(--bg);
  font: 600 9.5px/1 var(--mono);
}

/* The arrow carries the message, so it gets a label rather than an icon. */
.ba-arrow { display: grid; justify-items: center; gap: 6px; color: var(--fg-faint); }
.ba-arrow i {
  display: block; width: 46px; height: 1px; background: var(--line-2, var(--line)); position: relative;
}
.ba-arrow i::after {
  content: ""; position: absolute; right: 0; top: -3px;
  border: 3.5px solid transparent; border-left-color: var(--accent);
}
.ba-arrow span { font-size: 10px; letter-spacing: .05em; text-transform: uppercase; }

/* Capped, not full-width: a 2:3 result at column width is 700 px tall and
   pushes the references out of the same eyeful — and the comparison only works
   if both sides are visible at once. */
/* Left-aligned, not centred: the three parts must read as one continuous
   sentence — refs, arrow, result. Centring the result in a 1fr column opened a
   dead gap after the arrow and broke the reading line.
   NOT `justify-self: start` — that shrink-wraps the column, and the image's
   `max-width: 100%` then resolves against a width it is itself defining, so it
   collapses to nothing. The column stays stretched; the block image inside
   simply sits at its left edge. */
.ba-after { margin: 0; }
.ba-after img {
  /* 300, not 330: the card is as tall as this image, and the input column next
     to it stops around 250. Every pixel above that is a hole in the bottom
     left corner. */
  max-width: 100%; max-height: 300px; width: auto; display: block;
  border-radius: var(--radius);
  border: 1px solid var(--line); box-shadow: 0 10px 34px -18px rgb(0 0 0 / .55);
}
.ba-after figcaption { margin-top: 5px; font-size: 11px; color: var(--accent); letter-spacing: .03em; }

.ba-ask {
  margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--fg-dim);
  border-left: 2px solid var(--line); padding-left: 10px;
}
.ba-ask span {
  display: block; font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-faint); margin-bottom: 2px;
}
.ba .btn { justify-self: start; }

/* A member's picture wears its provenance. The point is not decoration: a
   visitor who can tell "someone like me made this with the same box" reads the
   whole gallery differently from one looking at a brochure. */
.tile.member .who em {
  font-style: normal; color: var(--accent);
  letter-spacing: .05em;
}
.tile.member { outline: 1px solid color-mix(in oklab, var(--accent) 26%, transparent); outline-offset: -1px; }

/* ── Preview panel ────────────────────────────────────────────────────────── */
/* The frame, the shutter and the brackets all come from js/orbite-fenetre.js.
   Everything here is only the CONTENT of the box. */

.o-fen-boite { border-radius: var(--radius-lg); }
.pv-corps { color: var(--fg); }

.pv {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(290px, 0.8fr);
  gap: 20px;
}

/* The thumbnail is painted as a blurred bed UNDER the full picture, so the box
   is never a white hole while the big file lands. It is the same bytes the
   mosaic already has in cache — the wait costs nothing and shows something. */
.pv-shot {
  position: relative;
  margin: 0;
  background: var(--bg-2);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;      /* the column is stretched by the grid; the picture
                               is not — it sits in the middle of its bed */
  justify-content: center;
  overflow: hidden;
}
/* A real blurred layer carrying the thumbnail, NOT backdrop-filter. Measured in
   Chromium on 2026-08-16: a backdrop-filter pseudo-element blurred the <img>
   sitting above it too, whatever its z-index — the panel opened on a smear. */
.pv-shot::before {
  content: '';
  position: absolute;
  inset: -30px;
  background: var(--ph) center / cover no-repeat;
  filter: blur(26px) saturate(0.8);
  opacity: 0.42;
  pointer-events: none;
}
.pv-shot img {
  position: relative;
  z-index: 1;
  width: 100%;
  height: auto;          /* the inline aspect-ratio gives the box its final size
                            BEFORE the file lands — no jump, and the flight has
                            a target to aim at */
  max-height: 60vh;
  object-fit: contain;
  display: block;
  animation: pv-land 520ms var(--o-lourd, ease) both;
}
/* A clip in the panel is sized by its own INTRINSIC pixels, not by width: 100 %.
   The reason is measured on the strip above (see `.clip video`): a 9:16 frame
   in a box that is 100 % wide and capped in height is a box that is no longer
   9:16, and `object-fit: contain` then pillars it with black either side. With
   both dimensions auto, a replaced element scales itself inside max-width /
   max-height and stays exactly its own shape — and the width/height ATTRIBUTES
   the panel writes give it that shape before a byte of the mp4 has landed. */
.pv-shot video {
  position: relative; z-index: 1;
  width: auto; height: auto; max-width: 100%; max-height: 60vh;
  display: block; background: #000;
  animation: pv-land 520ms var(--o-lourd, ease) both;
}
/* While the clicked picture is in the air, the one in the frame is not there
   yet. `animation: none` is load-bearing: a running animation outranks inline
   style, so hiding it any other way left two copies on screen. */
.pv-shot.flying img, .pv-shot.flying video { animation: none; opacity: 0; }
@keyframes pv-land { from { opacity: 0; filter: blur(8px); } to { opacity: 1; filter: none; } }

.pv-side { display: flex; flex-direction: column; gap: 14px; min-width: 0; }

.pv-cap { margin: 0; font-size: 15px; line-height: 1.35; color: var(--fg); }

.pv-ask h4 {
  margin: 0 0 6px;
  font: 700 9.5px/1 var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.pv-prompt {
  margin: 0;
  padding: 10px 12px;
  background: var(--bg-2);
  border-left: 2px solid var(--accent);
  font: 400 12.5px/1.55 var(--mono);
  color: var(--fg-dim);
  max-height: 8.5em;
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}
.pv-prompt::-webkit-scrollbar { width: 6px; }
.pv-prompt::-webkit-scrollbar-thumb { background: var(--line); }
.pv-prompt::-webkit-scrollbar-track { background: transparent; }
.pv-copy {
  margin-top: 7px;
  background: none;
  border: 1px solid var(--line);
  color: var(--fg-faint);
  padding: 5px 9px;
  font: 700 9.5px/1 var(--mono);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
}
.pv-copy:hover { color: var(--accent); border-color: var(--accent); }

/* A spec sheet, not a paragraph: a reader scanning for the size should find it
   without reading a sentence. */
.pv-spec { margin: 0; display: grid; gap: 1px; background: var(--line-soft); border: 1px solid var(--line-soft); }
.pv-spec > div { display: flex; align-items: baseline; gap: 10px; background: var(--bg-1); padding: 7px 10px; }
.pv-spec dt {
  flex: none; width: 62px;
  font: 700 9.5px/1.5 var(--mono);
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--fg-faint);
}
.pv-spec dd { margin: 0; font-size: 12.5px; color: var(--fg); }
.pv-spec dd i { font-style: normal; color: var(--fg-faint); margin-left: 6px; font-size: 11px; }

.pv-rate { display: flex; align-items: center; gap: 10px; }
.pv-star {
  background: none; border: none; padding: 0 1px; cursor: pointer;
  font-size: 19px; line-height: 1; color: var(--fg-faint);
  transition: color 140ms ease, transform 140ms var(--ease);
}
.pv-star.on { color: var(--accent); }
.pv-star.avg { color: color-mix(in oklab, var(--accent) 52%, var(--fg-faint)); }
.pv-stars:hover .pv-star { color: var(--fg-faint); }
.pv-stars:hover .pv-star:hover { transform: translateY(-1px); }
/* Hovering star 4 lights 1..4 — the row reads left to right, so the lit run has
   to grow from the left, which plain ~ selectors cannot express backwards. */
.pv-stars { display: flex; flex-direction: row-reverse; }
.pv-stars .pv-star:hover, .pv-stars .pv-star:hover ~ .pv-star { color: var(--accent); }
.pv-score { font: 700 10px/1 var(--mono); letter-spacing: 0.1em; color: var(--fg-faint); }

.pv-words { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.pv-words li { display: flex; gap: 8px; align-items: baseline; font-size: 12px; }
.pv-words q { color: var(--fg-dim); }
.pv-words cite { font-style: normal; color: var(--fg-faint); font-size: 11px; white-space: nowrap; }

/* ── Le fil de commentaires d'une image (15/09) ────────────────────────────
   Phil : « maybe you can add some comment section on each image ». Le fil a
   une hauteur PLAFONNÉE et défile en interne : une image très commentée ne
   doit pas repousser « Open in the studio » hors de la fenêtre — le bouton qui
   fait vivre la boutique passe avant les mots des autres. */
.pv-talk { display: grid; gap: 7px; }
.pv-talk-h { display: flex; align-items: baseline; gap: 7px; }
.pv-talk-h b { font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--fg-dim); }
.pv-talk-n::before { content: '· '; }
/* Vu à l'écran : sans compte, le titre affichait « WHAT PEOPLE SAID · » — un
   séparateur seul, qui se lit comme un bug. */
.pv-talk-n:empty { display: none; }
.pv-talk .pv-words { max-height: 168px; overflow-y: auto; scrollbar-width: thin; padding-right: 2px; }
.pv-words li.pv-mine q { color: var(--accent); }
.pv-words li.pv-empty { display: block; }
.pv-say { display: flex; gap: 6px; }
.pv-say-in {
  flex: 1 1 auto; min-width: 0; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: color-mix(in srgb, var(--bg-0, #0d0b12) 60%, transparent);
  color: inherit; font: inherit;
  /* 16 px minimum : sous ce seuil, Safari iOS agrandit toute la page à la mise
     au point et n'y revient jamais (règle du 15/09, mesurée sur companion). */
  font-size: 16px;
}
.pv-say-in:focus-visible { outline: none; border-color: var(--accent); }
.pv-say-go {
  flex: none; padding: 8px 12px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--accent); background: transparent; color: var(--accent);
  font: 700 11px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase;
}
.pv-say-go:hover { background: var(--accent); color: var(--accent-ink); }
.pv-say-msg[data-tone="ok"] { color: var(--accent); opacity: .9; }
.pv-say-msg[data-tone="bad"] { color: #ff6b6b; opacity: .95; }

.pv-acts { margin-top: auto; display: grid; gap: 6px; }
.pv-go {
  width: 100%; border: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 14px;
  background: var(--accent); color: var(--accent-ink);
  font: 700 11px/1 var(--mono); letter-spacing: 0.16em; text-transform: uppercase;
  text-decoration: none;
  transition: filter 160ms ease;
}
.pv-go span { transition: transform 220ms var(--ease); }
.pv-go:hover { filter: brightness(1.08); }
.pv-go:hover span { transform: translateX(4px); }
.pv-here:hover span { transform: translateY(3px); }
/* When both actions are offered, only one of them can be the loud one. The
   studio link steps back to an outline — it is the long way round now. */
.pv-go.pv-alt {
  background: transparent; color: var(--fg-dim);
  border: 1px solid var(--line);
}
.pv-go.pv-alt:hover { color: var(--fg); border-color: color-mix(in oklab, var(--accent) 50%, var(--line)); }

.pv-nav {
  margin-top: 16px; padding-top: 11px;
  border-top: 1px solid var(--line-soft);
  display: flex; align-items: center; justify-content: space-between;
}
.pv-nav button {
  background: none; border: 1px solid transparent; color: var(--fg-faint);
  padding: 5px 8px; cursor: pointer;
  font: 700 10px/1 var(--mono); letter-spacing: 0.14em; text-transform: uppercase;
  transition: color 160ms ease, border-color 160ms ease;
}
.pv-nav button:hover { color: var(--fg); border-color: var(--line); }
.pv-count { font: 700 10px/1 var(--mono); letter-spacing: 0.14em; color: var(--fg-faint); }
.pv-count i { font-style: normal; opacity: 0.45; margin: 0 2px; }

/* The direction of travel. ORBITE's cascade always enters from the left; here
   the side depends on which arrow you pressed, so the movement says WHERE you
   went in the gallery and not merely that something changed. Same doctrine as
   the view transitions in theme.css. */
.o-fen-corps[data-dir="back"] .pv-line { animation-name: pv-ligne-back; }
@keyframes pv-ligne-back { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }

/* The tile you clicked keeps a mark while the panel is up: coming back, the eye
   lands where it left. */
.tile.opened .tile-go { outline: 1px solid var(--accent); outline-offset: 2px; }

@media (max-width: 880px) {
  .pv { grid-template-columns: 1fr; }
  .pv-shot img, .pv-shot video { max-height: 42vh; }
}

/* The 18+ note on an adult landing page. It sits above the lede because it is
   the first thing that has to be true, not a disclaimer swept into the footer —
   and because the terms it states (synthetic subjects, consent, no minors) are
   the ones a payment processor and a search-engine reviewer both look for. */
.seo-page .age-note {
  margin: 0 0 18px;
  padding: 11px 14px;
  border: 1px solid color-mix(in oklab, var(--warn) 40%, var(--line));
  border-left-width: 3px;
  border-radius: 10px;
  background: color-mix(in oklab, var(--warn) 7%, transparent);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--fg-faint);
}
.seo-page .age-note b {
  color: color-mix(in oklab, var(--warn) 80%, var(--fg));
  letter-spacing: .04em;
}

/* ══ LA COULEUR ÉTRANGÈRE ═══════════════════════════════════════════════════
   Première version (supprimée) : des aplats cyan et rouges posés dans les
   marges de chaque section. Erreur de lecture des affiches — et vérifiée à
   l'œil : le bloc cyan mangeait le G de « Generate » et le rouge se posait en
   travers de l'illustration du hero. Sur une affiche, ces blocs sont des
   objets du décor ; sur une page qui défile, ce sont des volets qui recouvrent
   ce qu'on est venu lire.

   Ce qu'il faut garder de l'affiche, c'est le PRINCIPE, pas le format : il
   existe exactement UNE couleur qui n'appartient ni au fond ni au signal, et
   on la voit une fois. Pas un thème à trois couleurs — un noir/jaune avec un
   accroc.

   Donc, la règle, tenue à la main page par page :

     · Le CYAN ne décore pas, il suit le CURSEUR. Le champ qui a le focus sort
       du jaune. Il n'y a qu'un focus à la fois dans un document, donc le
       « une fois par page » est garanti par la structure, pas par la
       discipline. Au repos, la page est strictement bicolore. (La règle vit
       dans theme.css, avec `.cmp-bar` — c'est là qu'est la barre.)
     · Le ROUGE se pose sur UN bouton, sur UNE page, avec `.mark-red`. Ce n'est
       pas un état, c'est une marque : un filet, jamais un aplat, et jamais sur
       un bouton déjà jaune (deux signaux qui se disputent le même geste, ça
       fait zéro signal).

   Si une page n'a ni champ ni bouton qui mérite la marque, elle reste en
   noir/jaune. Une page sans accroc est un cas normal, pas un oubli. */
body { overflow-x: clip; }

/* Le filet rouge. `outline` et pas `border` : la marque se pose SANS toucher
   à la géométrie du bouton (pas de 2 px qui décalent tout ce qui l'entoure),
   et `outline-offset` la détache assez pour qu'on lise « quelque chose est
   posé autour » plutôt que « ce bouton a une grosse bordure ». */
.mark-red {
  outline: 2px solid var(--rouge);
  outline-offset: 3px;
}
/* Le jour n'a pas de lueur : sur l'acide, le rouge tient tout seul. La nuit en
   a une, très basse — c'est ce qui sépare du carton peint d'une enseigne, et
   la nuit est une enseigne. */
[data-theme="dark"] .mark-red { box-shadow: 0 0 26px -10px var(--rouge); }
@media (prefers-reduced-motion: no-preference) {
  /* La marque n'apparaît pas, elle se POSE : quatre crans, comme le reste du
     vocabulaire de la page (js/mosh.js). Une seule fois, au chargement. */
  .mark-red { animation: mark-latch 420ms steps(4, end) both; }
  @keyframes mark-latch {
    from { outline-color: transparent; outline-offset: 11px; }
    to   { outline-color: var(--rouge); outline-offset: 3px; }
  }
}

/* ── La preuve, juste sous la promesse ───────────────────────────────────────
   Trois images faites ICI, chacune avec la phrase exacte qui l'a produite
   (render() dans src/seo-pages.js les résout contre img/showcase/manifest.json).

   Le geste : au repos on ne lit que la légende, posée en bas de l'image. Au
   survol le bandeau MONTE sur l'image et découvre la phrase. Ce n'est pas un
   effet — c'est le rapport de cause à effet mis en mouvement : la phrase
   remonte sur la chose qu'elle a produite, et c'est exactement elle que le
   clic dépose dans la boîte du studio. Ce qui apparaît EST ce qu'on emporte.

   Première version (jetée) : légende + phrase tronquée à deux lignes sous
   l'image. Regardée dans le navigateur, `-webkit-line-clamp` ne prenait pas
   (Chrome rendait `display: flow-root`), donc la phrase débordait sous le bord
   de la carte et les trois cartes n'avaient pas la même hauteur. La hauteur
   fixe ci-dessous supprime les deux problèmes d'un coup, sans clamp.

   Sans survol (tactile), le bandeau reste ouvert : un doigt ne survole pas, et
   une phrase qu'on ne peut jamais lire ne vaut pas la peine d'être écrite. */
.proof {
  list-style: none; padding: 0; margin: 0 0 6px;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(206px, 1fr)); gap: 12px;
}
.proof a {
  position: relative; display: block; height: 252px; overflow: hidden; color: inherit;
  background: var(--bg-1); border: 1px solid var(--line-soft); border-radius: var(--radius-lg);
  transition: border-color var(--dur) var(--ease);
}
.proof a:hover, .proof a:focus-visible { border-color: var(--line); }
.proof img, .proof video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform 620ms var(--ease);
}
.proof a:hover img, .proof a:focus-visible img { transform: scale(1.04); }
/* Un clip ne grossit pas au survol : il PART. L'agrandissement est le geste de
   la vignette fixe, il ferait doublon avec le seul mouvement qui compte ici. */
.proof a:hover video, .proof a:focus-visible video { transform: none; }
/* La hauteur au repos n'est PAS un nombre. Première tentative : le bandeau
   traduit de `calc(100% - 42px)`, avec 42 pour « une ligne de légende ». Vu
   dans le navigateur : une légende sur deux lignes se faisait couper en bas
   (« The photoshoot you never / booked »), et une légende sur une ligne
   laissait dépasser un bout de la phrase. Le geste dépendait donc du texte.
   La grille à `0fr` le rend au contenu : la rangée de la phrase vaut zéro au
   repos et exactement sa hauteur ouverte, quelle que soit la légende. */
.proof .cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  display: grid; grid-template-rows: auto 0fr;
  padding: 11px 13px 12px;
  background: color-mix(in oklab, var(--bg-0) 86%, transparent);
  -webkit-backdrop-filter: blur(9px); backdrop-filter: blur(9px);
  border-top: 1px solid var(--line-soft);
  transition: grid-template-rows 430ms var(--ease);
}
.proof a:hover .cap, .proof a:focus-visible .cap { grid-template-rows: auto 1fr; }
.proof .cap b { font-size: 13.5px; font-weight: 600; line-height: 1.3; }
.proof .cap code {
  min-height: 0; overflow: hidden;
  font-size: 11.5px; line-height: 1.5; color: var(--fg-dim);
}
/* L'espace entre la légende et la phrase appartient à la phrase : posé en
   marge du bandeau, il resterait visible bandeau fermé et rouvrirait le trou
   de quelques pixels qu'on vient de supprimer. */
.proof .cap code::before { content: ""; display: block; height: 7px; }

@media (hover: none) {
  .proof .cap { grid-template-rows: auto 1fr; }
}

/* Les prompts d'exemple sont devenus des liens (ils chargent le studio). Le
   filet gauche était déjà accent : ce qui change au survol, c'est le CONTRASTE
   du texte — la phrase passe de « exemple » à « ce que je vais éditer ». */
.prompt-list a { display: block; color: inherit; }
.prompt-list li { transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease); }
.prompt-list li:hover, .prompt-list li:focus-within { background: var(--bg-2); border-color: var(--line); border-left-color: var(--accent); }
.prompt-list li:hover code, .prompt-list li:focus-within code { color: var(--fg); }

@media (prefers-reduced-motion: reduce) {
  .proof img, .proof video, .proof .cap { transition: none; }
  .proof a:hover img, .proof a:focus-visible img { transform: none; }
}

/* ── LA VITRINE 3D (Phil, 15/09 : « once we got everything 2d 3d rig(skelton)
   animation so we should have show case at the 1st page ») ─────────────────
   Quatre cartes de même hauteur, et c'est la HAUTEUR qui commande : une maille
   est un portrait en pied, une photo aussi, le squelette aussi — tout ce qui
   entre dans ces cadres est vertical. Même leçon que la bande de clips juste
   au-dessus, où une largeur déclarée avait fait courir la légende sous la
   vignette voisine. */
.sc3-strip { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; }
.sc3-card {
  /* `flex: 0 0` explicite : en `width` seule, un item flex se laisse écraser
     par son contenu et les quatre cartes sortaient à 181/181/150/161 px —
     quatre cadres inégaux pour une même chaîne, ce qui casse justement la
     lecture « une étape, puis la suivante ». Mesuré à l'écran. */
  position: relative; margin: 0; display: grid; grid-template-columns: 1fr; gap: 8px;
  cursor: pointer; flex: 0 0 auto; width: min(23vh, 200px); align-content: start;
}
.sc3-live {
  /* Largeur imposée : sans elle, la colonne de grille se cale sur le contenu
     et la carte du squelette sortait 20 px plus étroite que ses voisines —
     un SVG n'a pas la même largeur intrinsèque qu'une photo. */
  position: relative; width: 100%; height: 38vh; min-height: 260px; border-radius: 12px; overflow: hidden;
  background: color-mix(in srgb, var(--ink, #12121a) 92%, #fff);
  border: 1px solid var(--line, rgba(255,255,255,.12));
  display: grid; place-items: center; color: var(--accent, #ffd23f);
}
.sc3-live img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sc3-live model-viewer { width: 100%; height: 100%; display: block; background: transparent; }
/* La planche d'os respire : elle est fine, elle ne remplit pas son cadre.
   Sélecteur qualifié par la BALISE, et c'est un correctif, pas un style :
   `.sc3-rig` nommait à la fois le <svg> ET la carte (`sc3-` + la clé d'étape),
   donc ce `width: auto` écrasait la largeur de la carte — mesurée à 160 px
   contre 181 pour ses trois voisines, sur la prod. Même famille de bug que
   `.sc3-move` (la carte héritait de la capitalisation des boutons) : deux
   rôles ne partagent pas un nom. */
svg.sc3-rig { width: auto; height: 86%; opacity: .92; }
.sc3-n {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  width: 22px; height: 22px; border-radius: 999px; display: grid; place-items: center;
  font-size: 11px; font-weight: 700; background: rgba(0,0,0,.55); color: #fff;
  backdrop-filter: blur(4px);
}
.sc3-badge {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  font-size: 10.5px; font-weight: 700; letter-spacing: .03em;
  padding: 3px 8px; border-radius: 999px;
  background: rgba(0,0,0,.55); color: #fff; backdrop-filter: blur(4px);
}
.sc3-card figcaption { display: grid; gap: 1px; text-align: center; }
.sc3-card figcaption b { font-size: 13px; }
.sc3-card figcaption span { font-size: 11.5px; opacity: .68; }
/* Les gestes : deux mots sous la carte, cliquables sans ouvrir la carte. */
.sc3-moves { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
.sc3-moves .sc3-move {
  border: 1px solid var(--line, rgba(255,255,255,.14)); background: transparent;
  color: inherit; border-radius: 999px; padding: 2px 9px; font-size: 11px;
  cursor: pointer; opacity: .65; text-transform: capitalize;
}
.sc3-moves .sc3-move.on { opacity: 1; border-color: var(--accent, #ffd23f); font-weight: 600; }
/* Le chargement se VOIT : sans ça, sept mégaoctets passent pour un clic mort. */
.sc3-card.is-loading .sc3-live::after {
  content: ''; position: absolute; inset: auto 0 0 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--accent, #ffd23f), transparent);
  animation: sc3sweep 1.1s linear infinite;
}
@keyframes sc3sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
.sc3-flat {
  position: absolute; inset: auto 0 0 0; padding: 6px 8px; font-size: 10.5px;
  background: rgba(0,0,0,.6); color: #fff;
}
@media (max-width: 720px) {
  /* Deux par ligne sur un téléphone : quatre cartes à la file feraient une
     bande à faire défiler, et Phil ne fait pas défiler une vitrine. */
  .sc3-card { width: calc(50% - 8px); min-width: 0; }
  .sc3-live { height: 30vh; min-height: 190px; }
}
@media (prefers-reduced-motion: reduce) {
  .sc3-card.is-loading .sc3-live::after { animation: none; }
}
