Compare commits

..

2 Commits

Author SHA1 Message Date
ordinarthur
642747d762 docs(marketing): outils d'export logo + OG image + image LinkedIn
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 37s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m10s
Trois templates HTML autonomes pour générer les assets visuels sans
quitter le repo :

- `docs/logo-export.html` — outil interactif d'export du wordmark
  Rubis.pro en PNG (Bricolage Grotesque). 3 layouts (horizontal,
  vertical, texte seul), 5 fonds, 5 couleurs, taille 120 → 4096 px,
  format carré. Preset 120 px pour le logo de consent screen Google
  OAuth. Le canvas attend `document.fonts.ready` avant rendu pour
  éviter le fallback sans-serif.

- `docs/og-default-source.html` — recréation HTML pixel-perfect du
  `og-default.png` (1200×630) avec le nouveau wordmark `Rubis.pro` et
  `DSO*`. Export via Chrome DevTools "Capture node screenshot" pour
  remplacer `apps/landing/public/og-default.png`.

- `docs/linkedin-launch-source.html` — image carrée 1200×1200 pour
  un post de lancement LinkedIn. Format optimal pour le feed mobile
  (LinkedIn n'crop pas les carrés). Headline + stat-héros + brand.

Tous les gems sont des SVG inline (4 facettes + contour) — identité
visuelle 1:1 avec `<Gem/>` du SPA et `favicon.svg`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:29:44 +02:00
ordinarthur
3052a7e909 feat(landing): brand "Rubis.pro" + corrections copy
- Brand : suffixe `sur l'ongle` → `.pro` (attaché, muted, non italique).
  Propagé partout via `<Brand withSuffix>` — header/footer landing,
  topbar/sidebar SPA, login/signup/onboarding. Renforce l'identification
  brand requise par Google OAuth (gem seul jugé trop générique).

- Hero : dots groupés avec leur label (inline-flex) — ne se baladent plus
  en début de ligne au flex wrap. Ajout d'un topbar mock "Rubis.pro ·
  Tableau de bord" en haut de la carte hero pour identifier le mock comme
  un vrai dashboard. `DSO` → `DSO*`.

- Stats : "Trois chiffres qui devraient vous fâcher" → "Trois chiffres
  exorbitants". Sous-titre remplacé par "Et vous faites sûrement partie
  intégrante de ces enquêtes." (plus direct, moins gratuit).

- Promise : "Votre temps vaut plus que ça" → "Votre temps est plus
  précieux". Réécriture de l'amorce ("votre boîte / lundis soirs" →
  "votre entreprise / journées"). Ajout d'une ligne italique "Parfois
  moins, si votre plan par défaut est bien réglé".

- HowItWorks : Step 01 — suppression de "à la caisse" et de ", RIB"
  (l'OCR ne lit pas le RIB en V1). Step 03 — "La machine fait le reste"
  → "L'algorithme fait le reste".

- Gamification : suppression de "Pas un PDF abscons" (jargon inutile) et
  de "Et oui, on garde un classement amical" (le classement n'est pas en
  V1). `DSO` → `DSO*`.

- Footnotes : ajout de la définition DSO (Days Sales Outstanding) sous
  celle d'OCR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 14:48:17 +02:00
10 changed files with 1302 additions and 41 deletions

View File

@ -1,13 +1,19 @@
export function Footnotes() { export function Footnotes() {
return ( return (
<aside className="border-t border-line bg-cream"> <aside className="border-t border-line bg-cream">
<div className="max-w-[820px] mx-auto px-5 sm:px-8 py-8"> <div className="max-w-[820px] mx-auto px-5 sm:px-8 py-8 space-y-3">
<p className="text-[13.5px] text-ink-3 leading-relaxed"> <p className="text-[13.5px] text-ink-3 leading-relaxed">
<span className="text-rubis font-semibold mr-1.5">*</span> <span className="text-rubis font-semibold mr-1.5">*</span>
<b className="text-ink-2">OCR</b> pour <i>Optical Character Recognition</i>. La <b className="text-ink-2">OCR</b> pour <i>Optical Character Recognition</i>. La
reconnaissance automatique du texte sur un PDF ou une photo. La machine lit votre reconnaissance automatique du texte sur un PDF ou une photo. La machine lit votre
facture par-dessus votre épaule, en somme. facture par-dessus votre épaule, en somme.
</p> </p>
<p className="text-[13.5px] text-ink-3 leading-relaxed">
<span className="text-rubis font-semibold mr-1.5">*</span>
<b className="text-ink-2">DSO</b> pour <i>Days Sales Outstanding</i>. Le délai
moyen, en jours, entre l'émission d'une facture et son encaissement. Plus il est
bas, plus votre trésorerie respire.
</p>
</div> </div>
</aside> </aside>
); );

View File

@ -16,7 +16,7 @@ export function Gamification() {
<p className="mt-5 max-w-[680px] mx-auto text-[17px] text-cream/85 leading-relaxed"> <p className="mt-5 max-w-[680px] mx-auto text-[17px] text-cream/85 leading-relaxed">
À chaque relance que Rubis envoie à votre place, vous gagnez un rubis. À la fin du À chaque relance que Rubis envoie à votre place, vous gagnez un rubis. À la fin du
mois, vous voyez exactement combien d'heures vous avez récupérées. Pas un graphique mois, vous voyez exactement combien d'heures vous avez récupérées. Pas un graphique
de DSO. Pas un PDF abscons. Du temps. Concret. de DSO*. Du temps. Concret.
</p> </p>
<div className="mt-10 flex flex-col items-center gap-3"> <div className="mt-10 flex flex-col items-center gap-3">
@ -35,7 +35,7 @@ export function Gamification() {
</div> </div>
<p className="mt-10 max-w-[460px] mx-auto text-[14px] text-cream/70"> <p className="mt-10 max-w-[460px] mx-auto text-[14px] text-cream/70">
Et oui, on garde un classement amical. Les meilleurs utilisateurs libèrent{" "} Les meilleurs utilisateurs libèrent{" "}
<b className="text-cream">30 heures par mois</b>. Plus de quoi prendre un long <b className="text-cream">30 heures par mois</b>. Plus de quoi prendre un long
week-end. Toutes les 4 semaines. week-end. Toutes les 4 semaines.
</p> </p>

View File

@ -1,4 +1,4 @@
import { Button, Eyebrow, Gem, cn } from "@rubis/ui"; import { Brand, Button, Eyebrow, Gem, cn } from "@rubis/ui";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
const APP_URL = "https://app.rubis.pro"; const APP_URL = "https://app.rubis.pro";
@ -52,10 +52,14 @@ export function Hero() {
<Check size={14} className="text-rubis" aria-hidden /> <Check size={14} className="text-rubis" aria-hidden />
30 jours gratuits puis Free 5 factures 30 jours gratuits puis Free 5 factures
</span> </span>
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" /> <span className="inline-flex items-center gap-3">
<span>Hébergement souverain</span> <span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" /> Hébergement souverain
<span>Made in France 🇫🇷</span> </span>
<span className="inline-flex items-center gap-3">
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
Made in France 🇫🇷
</span>
</div> </div>
</div> </div>
@ -68,22 +72,31 @@ export function Hero() {
<div className="relative w-full max-w-[480px] mx-auto lg:ml-auto lg:mr-0"> <div className="relative w-full max-w-[480px] mx-auto lg:ml-auto lg:mr-0">
<div <div
className={cn( className={cn(
"bg-white border border-line rounded-card shadow-card", "bg-white border border-line rounded-card shadow-card overflow-hidden",
"p-6 sm:p-7 lg:p-8",
)} )}
> >
{/* Hero rubis */} {/* Topbar mock identifie la carte comme un dashboard Rubis.pro,
<div className="flex items-center gap-4 pb-5 border-b border-line"> pas juste un widget de chiffres flottants. */}
<Gem size={56} glow /> <div className="flex items-center justify-between px-6 sm:px-7 lg:px-8 py-3.5 border-b border-line bg-cream/40">
<div> <Brand withSuffix gemSize={18} />
<div className="font-display font-bold text-[32px] tracking-[-0.022em] leading-none text-ink"> <span className="text-[11px] uppercase tracking-[0.08em] font-semibold text-ink-3">
124 rubis Tableau de bord
</div> </span>
<div className="mt-1.5 text-[14px] text-ink-2"> </div>
<b className="text-ink">24 h 48</b> que vous n'avez pas passées à relancer.
<div className="p-6 sm:p-7 lg:p-8">
{/* Hero rubis */}
<div className="flex items-center gap-4 pb-5 border-b border-line">
<Gem size={56} glow />
<div>
<div className="font-display font-bold text-[32px] tracking-[-0.022em] leading-none text-ink">
124 rubis
</div>
<div className="mt-1.5 text-[14px] text-ink-2">
<b className="text-ink">24 h 48</b> que vous n'avez pas passées à relancer.
</div>
</div> </div>
</div> </div>
</div>
{/* KPIs */} {/* KPIs */}
<div className="grid grid-cols-2 gap-5 mt-5"> <div className="grid grid-cols-2 gap-5 mt-5">
@ -100,7 +113,7 @@ export function Hero() {
</div> </div>
<div> <div>
<div className="text-[10.5px] uppercase tracking-[0.06em] font-semibold text-ink-3"> <div className="text-[10.5px] uppercase tracking-[0.06em] font-semibold text-ink-3">
DSO DSO*
</div> </div>
<div className="mt-1.5 font-display font-bold text-[22px] tracking-[-0.015em] text-ink tabular-nums"> <div className="mt-1.5 font-display font-bold text-[22px] tracking-[-0.015em] text-ink tabular-nums">
38 j 38 j
@ -135,6 +148,7 @@ export function Hero() {
</li> </li>
</ul> </ul>
</div> </div>
</div>
</div> </div>
{/* Badge flottant — relatif au wrapper carte (max-w 480) */} {/* Badge flottant — relatif au wrapper carte (max-w 480) */}

View File

@ -17,7 +17,7 @@ export function HowItWorks() {
<Step <Step
num="01" num="01"
title="Vous importez vos factures." title="Vous importez vos factures."
body="PDF, photo prise depuis votre téléphone à la caisse, scan reçu par mail — peu importe. L'OCR* lit, extrait montant, client, échéance, RIB. Vous vérifiez. Vingt secondes par facture, montre en main." body="PDF, photo prise depuis votre téléphone, scan reçu par mail — peu importe. L'OCR* lit, extrait montant, client, échéance. Vous vérifiez. Vingt secondes par facture, montre en main."
> >
<DropzoneWidget /> <DropzoneWidget />
</Step> </Step>
@ -47,7 +47,7 @@ export function HowItWorks() {
Pendant que vous travaillez, Rubis envoie les emails au moment prévu, suit qui a Pendant que vous travaillez, Rubis envoie les emails au moment prévu, suit qui a
ouvert, qui n'a pas répondu, et avant chaque relance vous demande discrètement par ouvert, qui n'a pas répondu, et avant chaque relance vous demande discrètement par
email : « Cette facture a-t-elle é réglée ? ». Vous répondez en deux secondes. email : « Cette facture a-t-elle é réglée ? ». Vous répondez en deux secondes.
La machine fait le reste. L'algorithme fait le reste.
</p> </p>
} }
> >

View File

@ -7,14 +7,14 @@ export function Promise() {
<div className="text-center max-w-[820px] mx-auto"> <div className="text-center max-w-[820px] mx-auto">
<Eyebrow>Notre conviction</Eyebrow> <Eyebrow>Notre conviction</Eyebrow>
<blockquote className="mt-6 font-display font-bold text-ink leading-[1.05] tracking-[-0.03em] text-[40px] sm:text-[56px] lg:text-[64px]"> <blockquote className="mt-6 font-display font-bold text-ink leading-[1.05] tracking-[-0.03em] text-[40px] sm:text-[56px] lg:text-[64px]">
Votre temps vaut <em>plus que ça</em>. Votre temps est <em>plus précieux</em>.
</blockquote> </blockquote>
</div> </div>
<div className="mt-14 grid lg:grid-cols-[1.4fr_1fr] gap-10 lg:gap-16 items-start max-w-[1080px] mx-auto"> <div className="mt-14 grid lg:grid-cols-[1.4fr_1fr] gap-10 lg:gap-16 items-start max-w-[1080px] mx-auto">
<div className="space-y-5 md:text-justify hyphens-auto"> <div className="space-y-5 md:text-justify hyphens-auto">
<p className="text-[17.5px] leading-relaxed text-ink-2"> <p className="text-[17.5px] leading-relaxed text-ink-2">
Vous n'avez pas créé votre boîte pour passer vos lundis soirs à rédiger des Vous n'avez pas créé votre entreprise pour passer vos journées à rédiger des
relances polies. Pendant que vous écrivez "je me permets un petit rappel relances polies. Pendant que vous écrivez "je me permets un petit rappel
concernant", vous ne facturez pas, vous ne vendez pas, vous ne créez pas. concernant", vous ne facturez pas, vous ne vendez pas, vous ne créez pas.
</p> </p>
@ -24,6 +24,9 @@ export function Promise() {
<b className="text-ink">moins de 3</b>. Soit 5 heures de votre vie récupérées. <b className="text-ink">moins de 3</b>. Soit 5 heures de votre vie récupérées.
Toutes les semaines. Pour toujours. Toutes les semaines. Pour toujours.
</p> </p>
<p className="text-[15px] leading-relaxed text-ink-3 italic">
Parfois moins, si votre plan par défaut est bien réglé.
</p>
</div> </div>
<div className="bg-cream-2 border border-line rounded-card p-7"> <div className="bg-cream-2 border border-line rounded-card p-7">

View File

@ -25,11 +25,10 @@ export function Stats() {
<div className="text-center max-w-[640px] mx-auto mb-12"> <div className="text-center max-w-[640px] mx-auto mb-12">
<Eyebrow>L'état des paiements en France</Eyebrow> <Eyebrow>L'état des paiements en France</Eyebrow>
<h2 className="mt-4 font-display font-bold text-ink leading-[1.1] tracking-[-0.025em] text-[34px] sm:text-[44px]"> <h2 className="mt-4 font-display font-bold text-ink leading-[1.1] tracking-[-0.025em] text-[34px] sm:text-[44px]">
Trois chiffres qui devraient vous fâcher. Trois chiffres exorbitants.
</h2> </h2>
<p className="mt-4 text-[17px] text-ink-2 leading-relaxed"> <p className="mt-4 text-[17px] text-ink-2 leading-relaxed">
Si vous lisez ça, vous avez probablement une facture impayée à l'heure on parle. Et vous faites sûrement partie intégrante de ces enquêtes.
Vous n'êtes pas un cas isolé.
</p> </p>
</div> </div>

View File

@ -0,0 +1,305 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>LinkedIn launch image — Rubis.pro</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--rubis: #9F1239;
--rubis-deep: #771328;
--rubis-light: #C9415C;
--rubis-glow: #FBE4EA;
--cream: #FAF7F2;
--cream-2: #F4EFE7;
--ink: #1A1410;
--ink-2: #4A3F36;
--ink-3: #8B7E73;
--line: #E8E0D6;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #2a2520;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
gap: 20px;
font-family: "Inter", system-ui, sans-serif;
}
.instructions {
max-width: 900px;
color: #ddd;
font-size: 14px;
line-height: 1.6;
background: #1a1410;
padding: 16px 20px;
border-radius: 8px;
}
.instructions code {
background: #3a3530;
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, Menlo, monospace;
font-size: 13px;
}
/*
* #li : carré 1200×1200 — format optimal pour le feed LinkedIn mobile
* (LinkedIn affiche les images carrées sans crop, ce qui maximise la
* vertical real estate quand le user scrolle). Pour exporter : Chrome
* DevTools → Elements → sélectionner #li → clic droit → "Capture node
* screenshot".
*/
#li {
width: 1200px;
height: 1200px;
background: var(--cream);
position: relative;
overflow: hidden;
font-family: "Inter", system-ui, sans-serif;
color: var(--ink);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 90px 90px 80px;
}
/* Halo rubis en haut-droite — brand color signal sans dominer */
.halo {
position: absolute;
top: -300px;
right: -300px;
width: 800px;
height: 800px;
border-radius: 50%;
background: radial-gradient(circle, var(--rubis-glow) 0%, transparent 65%);
pointer-events: none;
}
/* Gem géant en filigrane derrière le headline — signature visuelle */
.gem-bg {
position: absolute;
bottom: -140px;
left: -140px;
width: 720px;
height: 720px;
opacity: 0.08;
transform: rotate(0deg);
pointer-events: none;
}
/* ============ Header (brand lockup) ============ */
.top {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
display: inline-flex;
align-items: center;
gap: 14px;
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 800;
font-size: 36px;
letter-spacing: -0.02em;
color: var(--ink);
line-height: 1;
}
.brand .gem {
width: 42px;
height: 42px;
}
.brand .suffix {
color: var(--ink-3);
font-weight: 800;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: white;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 14px;
font-weight: 600;
color: var(--ink-2);
}
.badge .dot {
width: 8px;
height: 8px;
background: #2d8a4e;
border-radius: 50%;
box-shadow: 0 0 0 3px rgba(45, 138, 78, 0.18);
}
/* ============ Centre — headline + sub ============ */
.center {
position: relative;
z-index: 2;
max-width: 920px;
}
h1 {
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 800;
font-size: 110px;
line-height: 0.98;
letter-spacing: -0.04em;
color: var(--ink);
}
h1 em {
font-style: italic;
color: var(--rubis);
font-weight: 800;
}
.sub {
margin-top: 32px;
font-size: 28px;
line-height: 1.4;
color: var(--ink-2);
font-weight: 400;
max-width: 760px;
}
.sub b { color: var(--ink); font-weight: 700; }
/* ============ Stat chip ============ */
.stat-row {
margin-top: 40px;
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.stat {
display: inline-flex;
align-items: baseline;
gap: 10px;
padding: 14px 22px;
background: var(--rubis);
color: var(--cream);
border-radius: 14px;
box-shadow: 0 8px 24px rgba(159, 18, 57, 0.25);
}
.stat .num {
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 800;
font-size: 36px;
letter-spacing: -0.02em;
line-height: 1;
}
.stat .label {
font-size: 17px;
font-weight: 500;
opacity: 0.9;
}
.stat-sep {
font-size: 22px;
color: var(--ink-3);
}
.stat-soft {
font-size: 20px;
color: var(--ink-2);
font-weight: 500;
}
/* ============ Footer ============ */
.bottom {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.domain {
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 700;
font-size: 32px;
letter-spacing: -0.02em;
color: var(--ink);
}
.domain .arrow {
color: var(--rubis);
margin-right: 8px;
}
.tag {
font-size: 17px;
color: var(--ink-3);
font-weight: 500;
}
</style>
</head>
<body>
<p class="instructions">
<strong>Comment exporter en PNG 1200×1200</strong> — ouvrir Chrome DevTools (<code>⌘⌥I</code>),
sélectionner la div <code>#li</code> dans l'arbre Elements, clic droit →
<em>Capture node screenshot</em>. Format optimal pour un post LinkedIn dans le feed mobile.
</p>
<div id="li">
<div class="halo"></div>
<!-- Gem fantôme en arrière-plan -->
<svg class="gem-bg" viewBox="0 0 100 100" fill="none">
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
<!-- Top : brand + statut -->
<div class="top">
<div class="brand">
<svg class="gem" viewBox="0 0 100 100" fill="none">
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
<span>Rubis<span class="suffix">.pro</span></span>
</div>
<span class="badge">
<span class="dot"></span>
En ligne
</span>
</div>
<!-- Centre : headline -->
<div class="center">
<h1>
Vos factures<br />
relancées<br />
<em>toutes seules</em>.
</h1>
<p class="sub">
Le SaaS de relance pour TPE-PME françaises. Drag-and-drop, OCR,
plans de relance automatiques.
</p>
<div class="stat-row">
<span class="stat">
<span class="num">5 h</span>
<span class="label">par semaine récupérées</span>
</span>
<span class="stat-sep">·</span>
<span class="stat-soft">8 h → moins de 3</span>
</div>
</div>
<!-- Bottom : domaine + tag -->
<div class="bottom">
<span class="domain">
<span class="arrow"></span>rubis.pro
</span>
<span class="tag">30 jours gratuits · sans carte bancaire</span>
</div>
</div>
</body>
</html>

585
docs/logo-export.html Normal file
View File

@ -0,0 +1,585 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Logo export — Rubis.pro</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
:root {
--rubis: #9F1239;
--rubis-deep: #771328;
--rubis-light: #C9415C;
--rubis-glow: #FBE4EA;
--cream: #FAF7F2;
--ink: #1A1410;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: "Inter", system-ui, sans-serif;
background: var(--cream);
color: var(--ink);
min-height: 100vh;
padding: 32px;
}
.app {
display: grid;
grid-template-columns: 320px 1fr;
gap: 32px;
max-width: 1400px;
margin: 0 auto;
}
h1 {
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 700;
font-size: 28px;
margin: 0 0 4px;
letter-spacing: -0.02em;
}
.sub {
font-size: 13px;
color: #6b5f57;
margin-bottom: 24px;
}
.panel {
background: white;
border: 1px solid #e8e0d6;
border-radius: 16px;
padding: 20px;
}
.controls h2 {
font-family: "Bricolage Grotesque", sans-serif;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6b5f57;
margin: 0 0 8px;
font-weight: 600;
}
.group { margin-bottom: 20px; }
.group:last-child { margin-bottom: 0; }
.seg {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.seg.three { grid-template-columns: repeat(3, 1fr); }
.seg button {
font-family: "Inter", sans-serif;
font-size: 13px;
padding: 8px 10px;
border: 1px solid #e8e0d6;
background: white;
color: var(--ink);
border-radius: 8px;
cursor: pointer;
transition: all 0.12s;
}
.seg button:hover { border-color: var(--rubis-light); }
.seg button.active {
background: var(--rubis);
color: white;
border-color: var(--rubis);
}
.swatches {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
}
.swatches button {
aspect-ratio: 1;
border: 2px solid #e8e0d6;
border-radius: 8px;
cursor: pointer;
transition: all 0.12s;
position: relative;
}
.swatches button.active {
border-color: var(--rubis);
box-shadow: 0 0 0 2px var(--rubis-glow);
}
.swatches button[data-bg="transparent"] {
background-image:
linear-gradient(45deg, #ddd 25%, transparent 25%),
linear-gradient(-45deg, #ddd 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ddd 75%),
linear-gradient(-45deg, transparent 75%, #ddd 75%);
background-size: 8px 8px;
background-position: 0 0, 0 4px, 4px -4px, -4px 0;
}
label {
display: flex;
justify-content: space-between;
font-size: 13px;
margin-bottom: 6px;
color: #6b5f57;
}
input[type="range"] {
width: 100%;
accent-color: var(--rubis);
}
input[type="text"] {
width: 100%;
padding: 8px 10px;
border: 1px solid #e8e0d6;
border-radius: 8px;
font-family: "Inter", sans-serif;
font-size: 13px;
}
input[type="text"]:focus {
outline: none;
border-color: var(--rubis);
box-shadow: 0 0 0 3px var(--rubis-glow);
}
.toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
background: var(--cream);
border-radius: 8px;
font-size: 13px;
cursor: pointer;
user-select: none;
}
.toggle input { margin: 0; accent-color: var(--rubis); }
.export {
width: 100%;
padding: 14px;
background: var(--rubis);
color: white;
border: none;
border-radius: 12px;
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: background 0.12s;
margin-top: 8px;
}
.export:hover { background: var(--rubis-deep); }
.preview {
display: flex;
flex-direction: column;
gap: 16px;
}
.preview-stage {
flex: 1;
min-height: 480px;
border: 1px dashed #d4cabe;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background-image:
linear-gradient(45deg, #f0e9de 25%, transparent 25%),
linear-gradient(-45deg, #f0e9de 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f0e9de 75%),
linear-gradient(-45deg, transparent 75%, #f0e9de 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}
#stage canvas {
max-width: 100%;
max-height: 100%;
box-shadow: 0 8px 30px rgba(26, 20, 16, 0.12);
border-radius: 4px;
}
.info {
font-size: 12px;
color: #6b5f57;
text-align: center;
}
.font-status {
font-size: 12px;
color: #6b5f57;
margin-top: 8px;
}
.font-status.loaded { color: #2d8a4e; }
</style>
</head>
<body>
<div class="app">
<aside class="controls">
<h1>Logo export</h1>
<p class="sub">Wordmark Rubis.pro · Bricolage Grotesque · PNG haute résolution</p>
<div class="panel">
<div class="group">
<h2>Texte</h2>
<input type="text" id="text" value="Rubis.pro" />
</div>
<div class="group">
<h2>Layout</h2>
<div class="seg three" id="layout">
<button data-layout="horizontal" class="active">Horizontal</button>
<button data-layout="vertical">Vertical</button>
<button data-layout="wordmark">Texte seul</button>
</div>
</div>
<div class="group">
<h2>Couleur du logo</h2>
<div class="seg three" id="fg">
<button data-fg="rubis" class="active">Rubis</button>
<button data-fg="ink">Encre</button>
<button data-fg="cream">Crème</button>
</div>
</div>
<div class="group">
<h2>Fond</h2>
<div class="swatches" id="bg">
<button data-bg="cream" style="background:#FAF7F2" class="active" title="Crème"></button>
<button data-bg="white" style="background:#ffffff" title="Blanc"></button>
<button data-bg="ink" style="background:#1A1410" title="Encre"></button>
<button data-bg="rubis" style="background:#9F1239" title="Rubis"></button>
<button data-bg="transparent" title="Transparent"></button>
</div>
</div>
<div class="group">
<h2>Graisse</h2>
<div class="seg three" id="weight">
<button data-weight="500">500</button>
<button data-weight="700" class="active">700</button>
<button data-weight="800">800</button>
</div>
</div>
<div class="group">
<h2>Taille export</h2>
<div class="seg" id="size-presets" style="grid-template-columns: repeat(5, 1fr); margin-bottom: 8px;">
<button data-size="120" title="Google OAuth consent (120×120)">120</button>
<button data-size="512">512</button>
<button data-size="1024">1024</button>
<button data-size="2048" class="active">2048</button>
<button data-size="4096">4096</button>
</div>
<label><span>Côté</span><span id="size-val">2048 × 2048 px</span></label>
<input type="range" id="size" min="120" max="4096" step="8" value="2048" />
</div>
<div class="group">
<label class="toggle">
<span>Padding interne</span>
<input type="checkbox" id="padding" checked />
</label>
</div>
<button class="export" id="export-btn">Télécharger en PNG</button>
<p class="font-status" id="font-status">Chargement de la police…</p>
</div>
</aside>
<main class="preview">
<div class="preview-stage">
<div id="stage"></div>
</div>
<p class="info" id="info"></p>
</main>
</div>
<script>
// --- State ---
const state = {
text: "Rubis.pro",
layout: "horizontal",
fg: "rubis",
bg: "cream",
weight: 700,
size: 2048,
padding: true,
};
const COLORS = {
rubis: "#9F1239",
"rubis-deep": "#771328",
"rubis-light": "#C9415C",
ink: "#1A1410",
cream: "#FAF7F2",
white: "#ffffff",
};
// --- Drawing ---
/**
* Dessine le gem facetté (4 facettes + contour) à la position (x, y) avec
* la taille `size` et la couleur `color`. Reproduit exactement <Gem/> du
* SPA / favicon.svg, mais en canvas — garantit l'identité visuelle 1:1.
*/
function drawGem(ctx, x, y, size, color) {
const s = size / 88; // viewBox interne 88x88 (de 6 à 94)
const cx = x + size / 2;
const cy = y + size / 2;
const tx = (px) => cx + (px - 50) * s;
const ty = (py) => cy + (py - 50) * s;
const facets = [
{ pts: [[50, 6], [6, 50], [50, 50]], alpha: 1.0 },
{ pts: [[50, 6], [94, 50], [50, 50]], alpha: 0.8 },
{ pts: [[6, 50], [50, 50], [50, 94]], alpha: 0.65 },
{ pts: [[50, 50], [94, 50], [50, 94]], alpha: 0.48 },
];
for (const { pts, alpha } of facets) {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(tx(pts[0][0]), ty(pts[0][1]));
ctx.lineTo(tx(pts[1][0]), ty(pts[1][1]));
ctx.lineTo(tx(pts[2][0]), ty(pts[2][1]));
ctx.closePath();
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(1.6 * s, 1);
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(tx(50), ty(6));
ctx.lineTo(tx(94), ty(50));
ctx.lineTo(tx(50), ty(94));
ctx.lineTo(tx(6), ty(50));
ctx.closePath();
ctx.stroke();
}
/**
* Construit un canvas avec le logo selon le state courant.
* Renvoie { canvas, width, height } — le canvas est dimensionné pour matcher
* le contenu (avec padding optionnel), pas forcé carré.
*/
function renderLogo(opts = {}) {
const exportSize = opts.size ?? state.size;
const dpr = opts.dpr ?? 1;
const fgColor = COLORS[state.fg];
const bgColor = state.bg === "transparent" ? null : COLORS[state.bg];
// Le wordmark est dimensionné en fonction de la largeur cible.
// Bricolage Grotesque a une cap-height ~0.72 et un descender ~0.22.
// Pour un rendu propre, on calcule d'abord la métrique du texte puis
// on positionne tout autour.
// Off-screen mesure
const measure = document.createElement("canvas").getContext("2d");
// En layout "wordmark" pur, le texte fait ~80% de la largeur.
// En "horizontal", gem prend ~0.18 de la largeur, gap ~0.05, texte le reste.
// En "vertical", gem au-dessus, texte en-dessous.
const padding = state.padding ? exportSize * 0.08 : 0;
let textHeightRatio; // hauteur du texte / hauteur totale du contenu
let gemSize, gap, contentW, contentH;
let fontSize;
if (state.layout === "wordmark") {
// Texte seul : on calibre la fontSize pour que le texte rendu fasse
// ~exportSize - 2*padding de large.
const targetW = exportSize - 2 * padding;
fontSize = 100;
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
const w0 = measure.measureText(state.text).width;
fontSize = (targetW / w0) * fontSize;
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
const m = measure.measureText(state.text);
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
contentW = m.width;
contentH = ascent + descent;
gemSize = 0;
gap = 0;
} else if (state.layout === "horizontal") {
// Gem + texte côte à côte. Hauteur du gem = hauteur du texte (cap-height-ish).
// On vise une largeur totale = exportSize - 2*padding.
const targetW = exportSize - 2 * padding;
// Premier passage : on suppose gem = hauteur du texte ≈ 0.78 * fontSize.
// gap = 0.18 * gemSize, contentW = gemSize + gap + textW.
// On calibre fontSize pour que tout ça fasse targetW.
fontSize = 100;
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
const w0 = measure.measureText(state.text).width;
// gem ≈ 0.78 * fontSize (proche de la cap-height)
// gap ≈ 0.22 * gem
const gemRatio = 0.85;
const gapRatio = 0.18;
const totalAtFs100 = gemRatio * fontSize + gapRatio * gemRatio * fontSize + w0;
fontSize = (targetW / totalAtFs100) * fontSize;
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
const m = measure.measureText(state.text);
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
gemSize = gemRatio * fontSize;
gap = gapRatio * gemSize;
contentW = gemSize + gap + m.width;
contentH = Math.max(gemSize, ascent + descent);
} else {
// vertical : gem au-dessus, texte en-dessous, centrés horizontalement
const targetW = exportSize - 2 * padding;
fontSize = 100;
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
const w0 = measure.measureText(state.text).width;
fontSize = (targetW / w0) * fontSize;
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
const m = measure.measureText(state.text);
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
gemSize = m.width * 0.42; // gem proportionné au texte
gap = gemSize * 0.25;
contentW = m.width;
contentH = gemSize + gap + ascent + descent;
}
// Canvas final : carré exportSize × exportSize, contenu centré.
const totalW = exportSize;
const totalH = exportSize;
const canvas = document.createElement("canvas");
canvas.width = Math.round(totalW * dpr);
canvas.height = Math.round(totalH * dpr);
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
// Fond
if (bgColor) {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, totalW, totalH);
}
// Dessin selon layout
ctx.fillStyle = fgColor;
ctx.textBaseline = "alphabetic";
ctx.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
if (state.layout === "wordmark") {
const m = ctx.measureText(state.text);
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
const x = (totalW - contentW) / 2;
const y = totalH / 2 - (ascent + descent) / 2 + ascent;
ctx.fillText(state.text, x, y);
} else if (state.layout === "horizontal") {
const m = ctx.measureText(state.text);
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
const textH = ascent + descent;
const startX = (totalW - contentW) / 2;
const centerY = totalH / 2;
// Gem centré verticalement
const gemY = centerY - gemSize / 2;
drawGem(ctx, startX, gemY, gemSize, fgColor);
// Texte aligné sur la baseline → on positionne pour que le centre vertical du texte = centerY
const textY = centerY - (ascent + descent) / 2 + ascent;
ctx.fillStyle = fgColor;
ctx.fillText(state.text, startX + gemSize + gap, textY);
} else {
// vertical
const m = ctx.measureText(state.text);
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
const gemX = (totalW - gemSize) / 2;
const gemY = (totalH - contentH) / 2;
drawGem(ctx, gemX, gemY, gemSize, fgColor);
ctx.fillStyle = fgColor;
const textX = (totalW - m.width) / 2;
const textY = gemY + gemSize + gap + ascent;
ctx.fillText(state.text, textX, textY);
}
return { canvas, width: totalW, height: totalH };
}
// --- Preview ---
function updatePreview() {
const stage = document.getElementById("stage");
stage.innerHTML = "";
// Preview = mêmes proportions, taille bornée pour l'affichage
const previewSize = Math.min(state.size, 1024);
const { canvas, width, height } = renderLogo({ size: previewSize, dpr: 2 });
// Cadre transparent visible si bg = transparent
stage.appendChild(canvas);
document.getElementById("info").textContent =
`Export : ${state.size} × ${Math.round((height / width) * state.size)} px`;
}
// --- Export ---
function exportPng() {
const { canvas } = renderLogo({ size: state.size, dpr: 1 });
const variant = [state.layout, state.fg, state.bg, `${state.size}px`].join("-");
const filename = `rubis-logo-${variant}.png`;
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}, "image/png");
}
// --- Wiring ---
function wireSegmented(containerId, key, type = "string") {
const container = document.getElementById(containerId);
container.addEventListener("click", (e) => {
const btn = e.target.closest("button");
if (!btn) return;
container.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
const val = btn.dataset[key];
state[key] = type === "number" ? Number(val) : val;
updatePreview();
});
}
document.getElementById("text").addEventListener("input", (e) => {
state.text = e.target.value || "Rubis.pro";
updatePreview();
});
wireSegmented("layout", "layout");
wireSegmented("fg", "fg");
wireSegmented("bg", "bg");
wireSegmented("weight", "weight", "number");
function setSize(value) {
state.size = Number(value);
document.getElementById("size").value = state.size;
document.getElementById("size-val").textContent = `${state.size} × ${state.size} px`;
const presets = document.getElementById("size-presets");
presets.querySelectorAll("button").forEach((b) => {
b.classList.toggle("active", Number(b.dataset.size) === state.size);
});
updatePreview();
}
document.getElementById("size").addEventListener("input", (e) => setSize(e.target.value));
document.getElementById("size-presets").addEventListener("click", (e) => {
const btn = e.target.closest("button");
if (btn) setSize(btn.dataset.size);
});
document.getElementById("padding").addEventListener("change", (e) => {
state.padding = e.target.checked;
updatePreview();
});
document.getElementById("export-btn").addEventListener("click", exportPng);
// --- Font loading ---
// On attend que Bricolage Grotesque soit chargée avant de rendre le canvas,
// sinon le navigateur fallback en sans-serif et le rendu est faux.
const status = document.getElementById("font-status");
Promise.all([
document.fonts.load('700 100px "Bricolage Grotesque"'),
document.fonts.load('800 100px "Bricolage Grotesque"'),
document.fonts.load('500 100px "Bricolage Grotesque"'),
]).then(() => {
status.textContent = "Police chargée — prêt à exporter.";
status.classList.add("loaded");
updatePreview();
});
</script>
</body>
</html>

355
docs/og-default-source.html Normal file
View File

@ -0,0 +1,355 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>OG default source — Rubis.pro</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--rubis: #9F1239;
--rubis-deep: #771328;
--rubis-light: #C9415C;
--rubis-glow: #FBE4EA;
--cream: #FAF7F2;
--cream-2: #F4EFE7;
--ink: #1A1410;
--ink-2: #4A3F36;
--ink-3: #8B7E73;
--line: #E8E0D6;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #2a2520;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
gap: 20px;
font-family: "Inter", system-ui, sans-serif;
}
.instructions {
max-width: 900px;
color: #ddd;
font-size: 14px;
line-height: 1.6;
background: #1a1410;
padding: 16px 20px;
border-radius: 8px;
}
.instructions code {
background: #3a3530;
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, Menlo, monospace;
font-size: 13px;
}
.instructions a { color: #FBE4EA; }
/*
* #og : EXACTEMENT 1200×630 (specs Open Graph).
* Pour exporter : Chrome DevTools → Inspecter cette div → clic droit
* → "Capture node screenshot". Le PNG sortira pile à 1200×630.
*/
#og {
width: 1200px;
height: 630px;
background: var(--cream);
position: relative;
overflow: hidden;
font-family: "Inter", system-ui, sans-serif;
color: var(--ink);
}
/* Halo rubis discret en haut-droite */
.halo {
position: absolute;
top: -200px;
right: -200px;
width: 600px;
height: 600px;
border-radius: 50%;
background: radial-gradient(circle, var(--rubis-glow) 0%, transparent 65%);
pointer-events: none;
}
/* Triangle fantôme en bas (clin d'œil au gem) */
.ghost-tri {
position: absolute;
bottom: -100px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 320px;
height: 320px;
background: var(--rubis);
opacity: 0.06;
border-radius: 12px;
}
.content {
position: relative;
height: 100%;
padding: 70px 80px;
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 60px;
align-items: center;
}
/* ============ Brand lockup ============ */
.brand {
display: inline-flex;
align-items: center;
gap: 12px;
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 800;
font-size: 28px;
letter-spacing: -0.02em;
color: var(--ink);
line-height: 1;
}
.brand .gem {
width: 32px;
height: 32px;
}
.brand .suffix {
color: var(--ink-3);
font-weight: 800;
}
/* ============ Headline ============ */
h1 {
margin-top: 32px;
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 800;
font-size: 70px;
line-height: 1.02;
letter-spacing: -0.035em;
color: var(--ink);
}
h1 em {
font-style: italic;
color: var(--rubis);
font-weight: 800;
}
.sub {
margin-top: 24px;
font-size: 22px;
line-height: 1.45;
color: var(--ink-2);
max-width: 520px;
}
.sub b { color: var(--ink); font-weight: 700; }
.cta {
margin-top: 32px;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 18px;
background: white;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 15px;
font-weight: 600;
color: var(--ink);
box-shadow: 0 4px 12px rgba(26, 20, 16, 0.06);
}
.cta .gem {
width: 16px;
height: 16px;
}
/* ============ Mock card ============ */
.mock {
background: white;
border: 1px solid var(--line);
border-radius: 16px;
box-shadow: 0 12px 40px rgba(26, 20, 16, 0.10);
overflow: hidden;
}
.mock-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 22px;
border-bottom: 1px solid var(--line);
background: rgba(244, 239, 231, 0.4);
}
.mock-topbar .brand { font-size: 16px; }
.mock-topbar .brand .gem { width: 18px; height: 18px; }
.mock-topbar .label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
color: var(--ink-3);
}
.mock-body { padding: 24px 26px; }
.mock-hero {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--line);
}
.mock-hero .gem-glow {
width: 56px;
height: 56px;
filter: drop-shadow(0 4px 16px rgba(159, 18, 57, 0.35));
}
.mock-count {
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 700;
font-size: 32px;
letter-spacing: -0.022em;
line-height: 1;
color: var(--ink);
}
.mock-count-sub {
margin-top: 6px;
font-size: 13px;
color: var(--ink-2);
}
.mock-count-sub b { color: var(--ink); font-weight: 700; }
.mock-kpis {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 16px;
}
.kpi-label {
font-size: 9.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
color: var(--ink-3);
}
.kpi-value {
margin-top: 4px;
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 700;
font-size: 22px;
letter-spacing: -0.015em;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.kpi-delta {
margin-top: 3px;
font-size: 10.5px;
color: var(--rubis);
font-weight: 500;
}
.mock-activity {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed var(--line);
font-size: 12px;
}
.mock-activity .text { color: var(--ink-2); }
.mock-activity b { color: var(--ink); font-weight: 600; }
.mock-activity time {
color: var(--ink-3);
font-variant-numeric: tabular-nums;
font-size: 10.5px;
}
</style>
</head>
<body>
<p class="instructions">
<strong>Comment exporter en PNG 1200×630</strong> — ouvrir Chrome DevTools (<code>⌘⌥I</code>),
sélectionner la div <code>#og</code>, clic droit dans l'arbre → <em>Capture node screenshot</em>.
Le PNG sort pile à 1200×630. Remplacer ensuite
<code>apps/landing/public/og-default.png</code>.
</p>
<div id="og">
<div class="halo"></div>
<div class="ghost-tri"></div>
<div class="content">
<div>
<div class="brand">
<svg class="gem" viewBox="0 0 100 100" fill="none">
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
<span>Rubis<span class="suffix">.pro</span></span>
</div>
<h1>
Vos factures relancées <em>toutes seules</em>.
</h1>
<p class="sub">
Le SaaS de relance pour TPE-PME françaises. <b>5 heures par semaine</b> récupérées, automatiquement.
</p>
<span class="cta">
<svg class="gem" viewBox="0 0 100 100" fill="none">
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
rubis.pro
</span>
</div>
<div class="mock">
<div class="mock-topbar">
<div class="brand">
<svg class="gem" viewBox="0 0 100 100" fill="none">
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
<span>Rubis<span class="suffix">.pro</span></span>
</div>
<span class="label">Tableau de bord</span>
</div>
<div class="mock-body">
<div class="mock-hero">
<svg class="gem-glow" viewBox="0 0 100 100" fill="none">
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
<div>
<div class="mock-count">124 rubis</div>
<div class="mock-count-sub"><b>24 h 48</b> libérées ce mois</div>
</div>
</div>
<div class="mock-kpis">
<div>
<div class="kpi-label">Encaissé</div>
<div class="kpi-value">14 320 €</div>
<div class="kpi-delta">+ 2 800 € vs avril</div>
</div>
<div>
<div class="kpi-label">DSO*</div>
<div class="kpi-value">38 j</div>
<div class="kpi-delta">6 j depuis Rubis</div>
</div>
</div>
<div class="mock-activity">
<span class="text">✓ Facture <b>F-2024-035</b> encaissée</span>
<time>10:02</time>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -2,8 +2,11 @@ import { cn } from "../lib/cn.js";
import { Gem } from "./Gem.js"; import { Gem } from "./Gem.js";
/** /**
* Lockup horizontal : + "Rubis" (+ optionnel "sur l'ongle" en suffixe italique muted). * Lockup horizontal : + "Rubis" (+ optionnel suffixe ".pro" muted, attaché
* À utiliser dans les headers, le sidebar, les emails. * au wordmark sans espace). À utiliser dans les headers, le sidebar, les
* emails. Le suffixe rend le mark identifiant (cf. exigence Google OAuth :
* un logo doit "identifier la marque de manière unique" la gem seule
* est trop générique).
* *
* Architecture : on s'appuie sur le composant <Gem/> SVG pas de PNG, pas * Architecture : on s'appuie sur le composant <Gem/> SVG pas de PNG, pas
* de border externe. La pierre EST le logo. Plus rien ne casse à l'export * de border externe. La pierre EST le logo. Plus rien ne casse à l'export
@ -13,7 +16,7 @@ import { Gem } from "./Gem.js";
* Cf. /docs/marque.md §2 et le pattern de la landing. * Cf. /docs/marque.md §2 et le pattern de la landing.
*/ */
type BrandProps = { type BrandProps = {
/** Affiche le suffixe "sur l'ongle" en italique muted. */ /** Affiche le suffixe ".pro" attaché au wordmark, en couleur muted. */
withSuffix?: boolean; withSuffix?: boolean;
/** /**
* Taille de la gem en pixels. Default 22 (lockup) ou 32 (onlyImage). * Taille de la gem en pixels. Default 22 (lockup) ou 32 (onlyImage).
@ -54,16 +57,7 @@ export function Brand({
<Gem size={resolvedSize} /> <Gem size={resolvedSize} />
<span className="leading-none"> <span className="leading-none">
Rubis Rubis
{withSuffix && ( {withSuffix && <span className="text-ink-3">.pro</span>}
<span
className={cn(
"ml-1 font-display italic font-medium text-ink-3",
"text-[12.5px] tracking-[-0.005em]",
)}
>
sur l&apos;ongle
</span>
)}
</span> </span>
</span> </span>
); );