diff --git a/.claude/skills/slide-variants/SKILL.md b/.claude/skills/slide-variants/SKILL.md
new file mode 100644
index 0000000..2dbf907
--- /dev/null
+++ b/.claude/skills/slide-variants/SKILL.md
@@ -0,0 +1,69 @@
+---
+name: slide-variants
+description: Use when building an Établi de l'IA séance deck from a storyboarded brief (D122) and Aurélien must compare 2–3 rendered treatments of a screen before choosing — « construis le deck », « montre-moi les variantes », « je prends B pour s1-e07 », « garde la A ». Authoring rules for the slides themselves stay in the `slides` skill.
+---
+
+# Variantes de slides (D122)
+
+The brief comes from app-formation-ai: `conception/generes/mandats//seances/seance-/brief.md`,
+generated by `build_deck.py` from the storyboard (`/deck-storyboard`). Each screen carries a
+Rôle and a Traitement; a screen marked **« À explorer »** carries 2–3 pistes. Deck-app builds
+every piste as a live variant; Aurélien picks by looking at them on screen.
+
+## Layout
+
+- One deck per folder: `src/decks/-s/index.tsx`, served at `//`
+ (`npm run dev`, then `http://localhost:5173/fb-s1/`). The path, not a query: the presenter tab
+ keeps only `?presenter=1`.
+- One file per capsule, `cNN-.tsx`, `export default [ … ]` in brief
+ order; `index.tsx` spreads them into ``. Capsule files can be built in parallel.
+- `theme.css` in the deck folder sets `:root` tokens from the mandat's `profil.md` (colors,
+ fonts); `ui.tsx` holds the deck's shared pieces (Kicker with the day's path, card styles, `n()`
+ for notes). Logos copied into `assets/`.
+
+## Variants
+
+```tsx
+
+
+ …
+ …
+ …
+
+
+```
+
+`V` on the live slide cycles; a badge shows `s1-e07 · B animée (2/3)`. The choice is kept per id
+in localStorage, shared with the presenter tab. Every variant renders the same content: D115
+holds — no fact, number or example the screen text lacks.
+
+## Choosing
+
+Aurélien says « s1-e07 : B ». Then, for that screen: delete the other ``s, inline the
+winner's JSX directly in the ``, drop the `` wrapper. A deck is final when
+`grep -r "` is empty. Report the choices back to the storyboard doc
+(one line per screen) so the next cohort's deck starts from them.
+
+## Slide-by-slide review (R)
+
+In dev, **R** or the dock button (speech bubble) opens a panel on the current slide: Décision
+`OK` / `Modifier` / `Retirer` + a note, saved by the Vite dev server (`/__review`, `vite.config.ts`)
+to `src/decks//revue.json`, keyed by screen id. On « applique la revue »:
+1. Read `revue.json`; handle every entry with `fait: false` and a decision other than `ok`
+ (`retirer` → delete the slide; `modifier` → do what the note says, D115 still holds).
+2. Set `fait: true` on each handled entry (keep the note), report one line per screen.
+3. Check the touched slides at 1280×720. A new note on a screen sets `fait` back to false.
+
+## Checks before handing over
+
+1. `npx tsc --noEmit -p .` clean.
+2. Every slide at 1280×720 (a Zoom share) in the browser: screenshot each, variants included
+ (press V). Fix clipped text, overflow, contrast, a lone left-anchored block (`slides` rule 3).
+3. Motion: nothing continuous, nothing faster than the engine's builds; 3D settles and stops.
+4. `grep -c "{/\* s1-e" ` equals the brief's screen count; no duration on screen.
+
+## Common mistakes
+
+- Variants that differ in content (one adds an example): they must differ in form only.
+- Inventing missing media or survey data instead of an empty, labelled frame.
+- Leaving `` in a deck that ships.
diff --git a/.gitignore b/.gitignore
index da431f1..6ffddfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
-node_modules/
+node_modules
dist/
.vite/
.DS_Store
.env
+public/media/
diff --git a/index.html b/index.html
index fb4074a..1118131 100644
--- a/index.html
+++ b/index.html
@@ -1,15 +1,15 @@
-
+
- Replace — your deck title
+ Séance 2 — Utiliser efficacement les modèles de langage
-
+
diff --git a/public/fb-s1/librechat.png b/public/fb-s1/librechat.png
new file mode 100644
index 0000000..e653773
Binary files /dev/null and b/public/fb-s1/librechat.png differ
diff --git a/src/App.tsx b/src/App.tsx
index f209b6d..55cbe4a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,977 +1,24 @@
-import Deck from './deck/Deck';
-import Slide from './deck/Slide';
-import Build from './deck/Build';
-import Reveal from './deck/Reveal';
-import Bento from './components/Bento';
-import Split from './components/Split';
-import CountUp from './components/CountUp';
-import TiltCard from './components/TiltCard';
-import Marquee from './components/Marquee';
-import VisualDashboard from './components/VisualDashboard';
-import StatGrid from './components/StatGrid';
-import Accordion from './components/Accordion';
-import Comparison from './components/Comparison';
-import Tabs from './components/Tabs';
-import Timeline from './components/Timeline';
-import CodeWindow from './components/CodeWindow';
-import BrowserFrame from './components/BrowserFrame';
-import SpotlightCard from './components/SpotlightCard';
-import { BarChart, LineChart, DonutChart } from './components/Charts';
-import Section from './components/Section';
-import Quote from './components/Quote';
-import Pricing from './components/Pricing';
-import Steps from './components/Steps';
-import Agenda from './components/Agenda';
-import Team from './components/Team';
-import Cover from './components/Cover';
-import BigNumber from './components/BigNumber';
-import Contrast from './components/Contrast';
-import Chat from './components/Chat';
-import Table from './components/Table';
-import Globe from './components/Globe';
+import { lazy, Suspense } from 'react';
+import type { ComponentType } from 'react';
-/* ══════════════════════════════════════════════════════════════════════
- ⚠️ THROWAWAY DEMO showing every component. DELETE these slides and AUTHOR
- THE USER'S DECK. Each child of is one slide. Add speaker notes with
- notes="…" on any slide (shown in presenter mode — press P).
- ══════════════════════════════════════════════════════════════════════ */
-const panel = (extra = 0.22): React.CSSProperties => ({
- position: 'absolute',
- inset: 0,
- background: `radial-gradient(120% 100% at 30% 20%, color-mix(in srgb, var(--primary) ${
- extra * 100
- }%, transparent), transparent 60%), var(--surface-2)`,
-});
-const card: React.CSSProperties = {
- padding: 22,
- borderRadius: 'var(--radius)',
- background: 'var(--surface)',
- border: '1px solid var(--hair)',
-};
+/* One deck per folder in src/decks//index.tsx, picked by the first path
+ segment (/fb-s1/ → src/decks/fb-s1). The path, not a ?query, because the
+ presenter tab is opened as pathname + '?presenter=1' and keeps only that. */
+const decks = import.meta.glob<{ default: ComponentType }>('./decks/*/index.tsx');
+const id = window.location.pathname.split('/').filter(Boolean)[0] || 's2-gc';
+const load = decks[`./decks/${id}/index.tsx`];
+const Deck = load ? lazy(load) : null;
export default function App() {
+ if (!Deck)
+ return (
+
+ Unknown deck « {id} ». Decks: {Object.keys(decks).map((k) => k.split('/')[2]).join(', ')}
+
+ );
return (
-
- {/* Cover */}
- Bolt Slides}
- subtitle="A responsive React deck engine. Delete this and build the real one."
- foot="June 2026 · Component demo"
- />
-
- {/* Statement + click-build */}
-
-
- Dashboards are everywhere.{' '}
- Insight isn't.
-
-
-
- Bolt Slides turns raw events into answers — automatically.
-
-
-
-
- {/* Agenda */}
-
-
- {/* Contrast — the problem */}
-
-
- {/* Split feature */}
-
- Everything, as it happens.
- >
- }
- body="Live metrics with sub-second latency — no pipelines to babysit."
- media={
- <>
-
-
-
-
-
-
- >
- }
- />
-
- {/* Bento */}
- ,
- body: 'events / min at peak.',
- c: 5,
- r: 2,
- variant: 'glow',
- },
- {
- k: 'Uptime',
- fig: ,
- c: 4,
- },
- { k: 'Regions', fig: , c: 3, variant: 'accent' },
- {
- k: 'Connectors',
- title: '120+ native',
- body: 'Snowflake, Kafka, dbt…',
- c: 4,
- },
- { k: 'Compliance', title: 'SOC 2 · HIPAA', c: 3 },
- ]}
- />
-
- {/* Globe */}
-
- Everywhere your data lives.
- >
- }
- body="Ingest close to the source; answer from the nearest edge."
- markers={[
- {
- location: [37.77, -122.41],
- size: 0.08,
- label: 'sfo1',
- value: '221k evt/s',
- },
- { location: [40.71, -74.0], size: 0.08 },
- {
- location: [51.5, -0.12],
- size: 0.07,
- label: 'lhr1',
- value: '188k evt/s',
- },
- { location: [52.52, 13.4], size: 0.05 },
- {
- location: [1.35, 103.82],
- size: 0.07,
- label: 'sin1',
- value: '96k evt/s',
- },
- { location: [35.68, 139.69], size: 0.06 },
- { location: [-33.87, 151.2], size: 0.05 },
- { location: [-23.55, -46.63], size: 0.05 },
- ]}
- arcs={[
- { from: [37.77, -122.41], to: [51.5, -0.12] },
- { from: [51.5, -0.12], to: [1.35, 103.82] },
- { from: [37.77, -122.41], to: [-23.55, -46.63] },
- ]}
- stats={[
- { value: '48%', label: 'North America' },
- { value: '31%', label: 'EMEA' },
- { value: '21%', label: 'APAC + LATAM' },
- ]}
- />
-
- {/* StatGrid — traction */}
- ,
- label: 'ARR',
- caption: 'up 3× year over year',
- },
- {
- value: ,
- label: 'Net retention',
- caption: 'best in class',
- },
- {
- value: ,
- label: 'Enterprise logos',
- caption: 'across six industries',
- },
- ]}
- />
-
- {/* BigNumber */}
- }
- caption="events answered in under a second."
- foot="Production traffic, trailing 30 days"
- />
-
- {/* Section divider */}
-
- How it works.
- >
- }
- />
-
- {/* Steps */}
-
-
- {/* Chat */}
-
-
- {/* Comparison */}
-
-
-
- Why teams switch
-
-
- The honest comparison.
-
-
-
-
-
-
-
-
-
- {/* Tabs */}
-
-
-
- One platform
-
-
- Built for every team.
-
-
-
-
- Trace any request end-to-end, alert on anomalies, ship with
- confidence.
-
- ),
- },
- {
- label: 'Data',
- content: (
-
-
-
- ),
- },
- {
- label: 'Ops',
- content: (
-
- One source of truth for uptime, cost, and capacity — no
- spreadsheets.
-
- ),
- },
- ]}
- />
-
-
-
- {/* Split + code */}
-
- Drop-in simple.
- >
- }
- body="Add it to your app in three lines. No SDK to learn, no schema to define."
- media={
- <>
-
-
-
-
- >
- }
- />
-
- {/* Browser frame */}
-
-
-
- See it live
-
-
- Your data, one screen.
-
-
-
-
-
-
-
-
- Bolt Slides
-
- {['Overview', 'Events', 'Funnels', 'Cohorts', 'Settings'].map(
- (n, i) => (
-
- {n}
-
- )
- )}
-
-
-
-
- Overview
-
- Last 30 days
-
-
- {[
- ['Revenue', '$1.24M', '▲ 18.2%'],
- ['Active users', '48,210', '▲ 9.4%'],
- ['Churn', '1.9%', '▼ 0.6%'],
- ].map(([l, v, d]) => (
-
-
- {l}
-
-
- {v}
-
-
- {d}
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
- {/* Charts */}
-
-
-
- The numbers
-
-
- Growth you can see.
-
-
-
-
-
-
- Weekly active
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Data table */}
-
-
-
- Unit economics
-
-
- Growth, by region.
-
-
-
-
-
-
-
- {/* Timeline */}
-
-
-
- Where we're going
-
-
- The roadmap.
-
-
-
-
-
-
-
- {/* Pricing */}
-
-
- {/* Spotlight principles */}
-
-
-
- What we believe
-
-
- Three principles.
-
-
-
-
- {[
- {
- k: '01',
- t: 'Fast by default',
- d: 'Speed is a feature. Everything is realtime.',
- },
- {
- k: '02',
- t: 'Yours to own',
- d: 'Your data, your infra, no lock-in.',
- },
- {
- k: '03',
- t: 'Honest pricing',
- d: 'No per-seat tax. Scale without surprises.',
- },
- ].map((p) => (
-
-
- {p.k}
-
-
- {p.t}
-
-
- {p.d}
-
-
- ))}
-
-
-
-
- {/* Accordion — FAQ */}
-
-
-
- Common questions
-
-
- Frequently asked.
-
-
-
-
-
-
-
- {/* Team */}
-
-
- {/* Logos */}
-
-
-
- Trusted by teams everywhere
-
-
-
-
-
- {/* Quote */}
-
-
- {/* CTA */}
-
-
-
- Let's talk.
-
-
- hello@bolt.new
-
-
-
-
+
+
+
);
}
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx
new file mode 100644
index 0000000..51e820f
--- /dev/null
+++ b/src/components/Prompt.tsx
@@ -0,0 +1,88 @@
+import { useState, type MouseEvent, type ReactNode } from 'react';
+
+/* A prompt to copy, verbatim: mono text in a bordered box with a copy button.
+ `label` names it (e.g. "Version 1"); `muted` styles a "before" prompt. */
+export default function Prompt({
+ children,
+ label = 'Prompt à copier',
+ muted,
+ size = 'md',
+}: {
+ children: ReactNode;
+ label?: string;
+ muted?: boolean;
+ size?: 'sm' | 'md' | 'lg';
+}) {
+ const [copied, setCopied] = useState(false);
+ const fontSize = {
+ sm: 'clamp(11.5px, 1.05vw, 15px)',
+ md: 'clamp(13px, 1.3vw, 18px)',
+ lg: 'clamp(16px, 2vw, 28px)',
+ }[size];
+ const copy = (e: MouseEvent) => {
+ const text =
+ e.currentTarget.parentElement?.parentElement?.querySelector('pre')
+ ?.innerText ?? '';
+ navigator.clipboard?.writeText(text).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ });
+ };
+ return (
+
+
+ {label}
+
+ {copied ? 'Copié' : 'Copier'}
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/Review.tsx b/src/components/Review.tsx
new file mode 100644
index 0000000..3a86594
--- /dev/null
+++ b/src/components/Review.tsx
@@ -0,0 +1,121 @@
+import { useEffect, useState } from 'react';
+
+/* Slide review (dev only, R or the dock button): a decision and a note per
+ screen id, saved to src/decks//revue.json through the dev server.
+ Claude reads that file on « applique la revue », does the work and sets
+ `fait: true`; a new note on the same screen reopens it. */
+export type Decision = 'ok' | 'modifier' | 'retirer';
+export type Entry = { slide: number; decision: Decision; note: string; fait: boolean; maj: string };
+type Revue = Record;
+
+const deck = window.location.pathname.split('/').filter(Boolean)[0] || 's2-gc';
+const url = `/__review?deck=${deck}`;
+const LABELS: Record = { ok: 'OK', modifier: 'Modifier', retirer: 'Retirer' };
+
+export default function Review({ id, slide, onClose }: { id: string; slide: number; onClose: () => void }) {
+ const [revue, setRevue] = useState({});
+ const [decision, setDecision] = useState('ok');
+ const [note, setNote] = useState('');
+ const [etat, setEtat] = useState<'' | 'enregistré' | 'erreur'>('');
+
+ useEffect(() => {
+ fetch(url)
+ .then((r) => r.json() as Promise)
+ .then((r) => {
+ setRevue(r);
+ setDecision(r[id]?.decision ?? 'ok');
+ setNote(r[id]?.note ?? '');
+ setEtat('');
+ })
+ .catch(() => setEtat('erreur'));
+ }, [id]);
+
+ const save = async () => {
+ const next = { ...revue, [id]: { slide: slide + 1, decision, note, fait: false, maj: new Date().toISOString() } };
+ const r = await fetch(url, { method: 'POST', body: JSON.stringify(next) }).catch(() => null);
+ if (r?.ok) setRevue(next);
+ setEtat(r?.ok ? 'enregistré' : 'erreur');
+ };
+
+ const e = revue[id];
+ const ouvertes = Object.values(revue).filter((x) => !x.fait && x.decision !== 'ok').length;
+
+ return (
+ {
+ k.stopPropagation();
+ if (k.key === 'Escape') onClose();
+ if (k.key === 'Enter' && (k.metaKey || k.ctrlKey)) save();
+ }}
+ style={{
+ position: 'fixed',
+ right: 16,
+ bottom: 88,
+ zIndex: 60,
+ width: 'min(360px, calc(100vw - 32px))',
+ display: 'grid',
+ gap: 10,
+ padding: 16,
+ borderRadius: 'var(--radius-sm)',
+ background: 'var(--surface)',
+ border: '1px solid var(--hair)',
+ boxShadow: 'var(--shadow)',
+ color: 'var(--fg)',
+ font: '14px/1.4 var(--font-body)',
+ }}
+ >
+
+
+ Revue · {id} (slide {slide + 1})
+
+
+ ✕
+
+
+ {e?.fait &&
Traité par Claude — une nouvelle note rouvre l'écran. }
+
+ {(Object.keys(LABELS) as Decision[]).map((d) => (
+ setDecision(d)} style={btn(decision === d)}>
+ {LABELS[d]}
+
+ ))}
+
+
+ );
+}
+
+const btn = (on: boolean) => ({
+ padding: '6px 12px',
+ borderRadius: 999,
+ border: '1px solid var(--hair)',
+ background: on ? 'var(--primary)' : 'transparent',
+ color: on ? 'var(--accent-ink)' : 'var(--fg)',
+ font: '600 13px/1.4 var(--font-body)',
+ cursor: 'pointer',
+});
diff --git a/src/components/Variants.tsx b/src/components/Variants.tsx
new file mode 100644
index 0000000..f0d2ec2
--- /dev/null
+++ b/src/components/Variants.tsx
@@ -0,0 +1,78 @@
+import { Children, useEffect, useState } from 'react';
+import type { ReactNode } from 'react';
+import { useDeck } from '../deck/DeckContext';
+
+/* Storyboard variants (D122): a screen marked « à explorer » renders 2–3
+ candidate treatments, one per piste. Press V on the live slide to cycle;
+ the choice is kept per screen id in localStorage (shared with the
+ presenter tab). Once Aurélien picks, the losing variants are deleted and
+ the winner is inlined: never ships in a final deck. */
+export function Variant({ children }: { label: string; children: ReactNode }) {
+ return <>{children}>;
+}
+
+const key = (id: string) => `deck:variant:${id}`;
+const read = (id: string) => {
+ try {
+ return Number(localStorage.getItem(key(id))) || 0;
+ } catch {
+ return 0;
+ }
+};
+
+export default function Variants({ id, children }: { id: string; children: ReactNode }) {
+ const items = Children.toArray(children) as { props: { label: string } }[];
+ const { isStatic } = useDeck();
+ const [i, setI] = useState(() => read(id) % items.length);
+
+ useEffect(() => {
+ const onStorage = (e: StorageEvent) => e.key === key(id) && setI(read(id) % items.length);
+ window.addEventListener('storage', onStorage);
+ if (isStatic) return () => window.removeEventListener('storage', onStorage);
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key !== 'v' || e.metaKey || e.ctrlKey || e.target instanceof HTMLInputElement) return;
+ setI((c) => {
+ const n = (c + 1) % items.length;
+ try {
+ localStorage.setItem(key(id), String(n));
+ } catch {
+ /* private mode: the choice just isn't remembered */
+ }
+ return n;
+ });
+ };
+ window.addEventListener('keydown', onKey);
+ return () => {
+ window.removeEventListener('storage', onStorage);
+ window.removeEventListener('keydown', onKey);
+ };
+ }, [id, isStatic, items.length]);
+
+ return (
+ <>
+ {/* ponytail: switching variants keeps the larger click count the engine
+ already registered for this slide; reload the slide if a build is skipped. */}
+
+ {items[i] as ReactNode}
+
+ {!isStatic && (
+
+ {id} · {items[i].props.label} ({i + 1}/{items.length}) · V
+
+ )}
+ >
+ );
+}
diff --git a/src/deck/Deck.tsx b/src/deck/Deck.tsx
index 8b9e26d..8c360ed 100644
--- a/src/deck/Deck.tsx
+++ b/src/deck/Deck.tsx
@@ -21,14 +21,16 @@ import {
IconShrink,
IconPresent,
IconClose,
+ IconReview,
} from './icons';
+import Review from '../components/Review';
/* ── The paged presentation engine + the Slidev-style chrome (dock + rail).
Wrap your //… in . Each top-level child is one slide.
→ / ↓ / Space next (reveals the next , then the next slide)
← / ↑ previous S sidebar G grid view
Home / End first / last A annotate P presenter (new tab)
- F fullscreen H hide/show the UI
+ F fullscreen H hide/show the UI R review (dev only)
Copy verbatim; theme only via the :root tokens. ───────────────────────── */
const fmt = (s: number) =>
@@ -99,6 +101,7 @@ export default function Deck({ children }: { children: ReactNode }) {
const [railOpen, setRailOpen] = useState(false);
const [gridOpen, setGridOpen] = useState(false);
const [drawing, setDrawing] = useState(false);
+ const [reviewing, setReviewing] = useState(false);
const [elapsed, setElapsed] = useState(0);
const [fs, setFs] = useState(false);
const [uiHidden, setUiHidden] = useState(false);
@@ -247,6 +250,10 @@ export default function Deck({ children }: { children: ReactNode }) {
case 'P':
openPresenter();
break;
+ case 'r':
+ case 'R':
+ if (import.meta.env.DEV) setReviewing((v) => !v);
+ break;
case 'h':
case 'H':
setUiHidden((v) => !v);
@@ -255,6 +262,7 @@ export default function Deck({ children }: { children: ReactNode }) {
setRailOpen(false);
setGridOpen(false);
setDrawing(false);
+ setReviewing(false);
setUiHidden(false);
break;
}
@@ -488,6 +496,13 @@ export default function Deck({ children }: { children: ReactNode }) {
)}
+ {reviewing && !isPresenter && (
+ setReviewing(false)}
+ />
+ )}
{/* phones: nav floats bare above the tools pill (see base.css) */}
{navCluster}
@@ -522,6 +537,17 @@ export default function Deck({ children }: { children: ReactNode }) {
>
+ {import.meta.env.DEV && (
+
setReviewing((v) => !v)}
+ >
+
+
+ )}
(
);
+export const IconReview = () => (
+
+
+
+
+);
diff --git a/src/decks/fb-s1/assets/logo-ets-formation.png b/src/decks/fb-s1/assets/logo-ets-formation.png
new file mode 100644
index 0000000..b7f01b9
Binary files /dev/null and b/src/decks/fb-s1/assets/logo-ets-formation.png differ
diff --git a/src/decks/fb-s1/assets/logo-formabois.png b/src/decks/fb-s1/assets/logo-formabois.png
new file mode 100644
index 0000000..101815e
Binary files /dev/null and b/src/decks/fb-s1/assets/logo-formabois.png differ
diff --git a/src/decks/fb-s1/assets/logo.png b/src/decks/fb-s1/assets/logo.png
new file mode 100644
index 0000000..1bc5137
Binary files /dev/null and b/src/decks/fb-s1/assets/logo.png differ
diff --git a/src/decks/fb-s1/assets/plateforme-apprenant.png b/src/decks/fb-s1/assets/plateforme-apprenant.png
new file mode 100644
index 0000000..369dae0
Binary files /dev/null and b/src/decks/fb-s1/assets/plateforme-apprenant.png differ
diff --git a/src/decks/fb-s1/c01-ouverture.tsx b/src/decks/fb-s1/c01-ouverture.tsx
new file mode 100644
index 0000000..ca99a27
--- /dev/null
+++ b/src/decks/fb-s1/c01-ouverture.tsx
@@ -0,0 +1,192 @@
+import type { CSSProperties } from 'react';
+import Slide from '../../deck/Slide';
+import Reveal from '../../deck/Reveal';
+import { JOUR, Kicker, card, n } from './ui';
+import logoEts from './assets/logo-ets-formation.png';
+import logoMhosaic from './assets/logo.png';
+import logoFormabois from './assets/logo-formabois.png';
+
+/* Capsule « Ouverture » — s1-e01 to s1-e04. */
+
+/* s1-e03 « Le parcours » — the five séance titles of the mandat (brief, from the sommaires). */
+const PARCOURS: { n: number; what: string; where?: string }[] = [
+ { n: 1, what: "Comprendre l'IA, ses possibilités et ses limites", where: 'Aujourd’hui' },
+ { n: 2, what: 'Utiliser efficacement les modèles de langage' },
+ { n: 3, what: 'Travailler avec des fichiers, des données et des assistants spécialisés' },
+ { n: 4, what: 'Utiliser plusieurs outils' },
+ { n: 5, what: 'Identifier la valeur, bâtir des cas d’affaires et préparer une feuille de route' },
+];
+
+/* s1-e03 — five chevrons, a road read left to right (not the staircase of « L'échelle »). */
+const NOTCH = 'clamp(14px, 1.6vw, 24px)';
+const chevron = (first: boolean) =>
+ first
+ ? `polygon(0 0, calc(100% - ${NOTCH}) 0, 100% 50%, calc(100% - ${NOTCH}) 100%, 0 100%)`
+ : `polygon(0 0, calc(100% - ${NOTCH}) 0, 100% 50%, calc(100% - ${NOTCH}) 100%, 0 100%, ${NOTCH} 50%)`;
+
+const logo: CSSProperties = { height: 'clamp(36px, 6vh, 64px)', width: 'auto', objectFit: 'contain' };
+
+const INSTALL = [
+ 'Sur la plateforme : « Je suis présent ».',
+ 'LibreChat ouvert dans un onglet.',
+ 'Au micro : votre prénom, votre métier, en une phrase.',
+];
+
+export default [
+ /* s1-e01 */
+
+
+
+
+
+
+ Séance 1 · en direct sur Zoom
+
+
+
+
+ Comprendre l'IA, ses possibilités et ses limites
+
+
+
+
+ L'Établi de l'IA est un parcours Mhosaic, offert avec ÉTS Formation pour Formabois, le comité sectoriel de
+ main-d'œuvre des industries de la transformation du bois.
+
+
+ ,
+
+ /* s1-e02 */
+
+
+ On s'installe
+
+ {INSTALL.map((t, i) => (
+
+
+ {i + 1}
+
+ {t}
+
+ ))}
+
+
+ Accès, tâche ou onboarding pas encore faits ? Dites-le dans le clavardage : on regarde ça ensemble avant le premier exercice.
+
+ ,
+
+ /* s1-e03 */
+
+ Le parcours
+ Cinq séances, une étape à la fois
+
+ {PARCOURS.map((s, i) => {
+ const lit = i === 0;
+ return (
+
+
+ {s.where ?? '·'}
+
+
+ Séance
+
+ {s.n}
+
+ {s.what}
+
+
+ );
+ })}
+
+ ,
+ /* s1-e04 */
+
+ Aujourd'hui
+ Le chemin d'aujourd'hui
+
+
+ {JOUR.map((step, i) => (
+
+
+
+ {step}
+
+
+ ))}
+
+ ,
+];
diff --git a/src/decks/fb-s1/c02-pourquoi.tsx b/src/decks/fb-s1/c02-pourquoi.tsx
new file mode 100644
index 0000000..ca4d0c4
--- /dev/null
+++ b/src/decks/fb-s1/c02-pourquoi.tsx
@@ -0,0 +1,278 @@
+import type { CSSProperties } from 'react';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import CountUp from '../../components/CountUp';
+import { useState } from 'react';
+import { Kicker, card, source, n } from './ui';
+import shotPlateforme from './assets/plateforme-apprenant.png';
+
+/* Capsule « Pourquoi vous êtes ici » — s1-e05 to s1-e08. */
+
+const body: CSSProperties = { fontSize: 'clamp(15px, 1.55vw, 20px)', color: 'var(--fg-muted)', lineHeight: 1.45 };
+const h3: CSSProperties = { fontSize: 'clamp(17px, 1.7vw, 22px)', marginBottom: 4 };
+
+/* ── s1-e07 content ── */
+const QUESTIONS: { title: string; text: string }[] = [
+ { title: 'La sécurité', text: "Ce que vous écrivez dans un outil d'IA public sort de chez vous. Tout à l'heure : ce qu'on lui donne, et ce qu'on ne lui donne jamais." },
+ { title: 'Où vont les données', text: "LibreChat, l'outil de votre formation, est hébergé par Mhosaic, au Canada. Les modèles qu'il interroge restent américains." },
+ { title: 'Ce que ça coûte', text: "L'outil tel quel : de zéro à environ trente dollars par mois et par personne. Pendant la formation, rien. Plus cher ? En séance 5, si ça en vaut la peine." },
+ { title: 'Les solutions d’ici', text: 'Il existe des hébergeurs et des intégrateurs québécois, et votre outil d’aujourd’hui est hébergé au Canada. Les grands modèles, eux, ne sont pas d’ici.' },
+];
+const SOURCE = 'Source : Stanford AI Index 2026, p. 365.';
+
+const Tile = ({ q, style }: { q: { title: string; text: string }; style?: CSSProperties }) => (
+
+);
+
+/* ── s1-e08 content ── */
+const HAS = ['brillant et rapide', 'disponible jour et nuit', 'ne se vexe jamais'];
+const LACKS = ['aucune idée de votre entreprise', 'il improvise quand la consigne est floue', "rien de ce qu'il écrit n'est vérifié"];
+const SEANCES = ['le comprendre', 'lui parler', 'lui donner votre matière', 'le retrouver dans vos outils', 'décider où il vaut son salaire'];
+
+const Icon = ({ ok }: { ok: boolean }) => (
+
+
+
+);
+const Seances = () => (
+
+
Tout le parcours : apprendre à bien diriger ce stagiaire.
+
+ {SEANCES.map((s, i) => (
+
+ {i + 1} · {s}
+
+ ))}
+
+
+);
+const Title = () => (
+
+ Un stagiaire surdoué et inépuisable
+
+);
+
+/** B — a gold stamp over the employee card. */
+const Stamp = ({ at, text, rot }: { at: number; text: string; rot: number }) => (
+
+
+ {text}
+
+
+);
+
+/* « Vos deux outils » — added in review (between « Ce que vous m'avez dit » and « Vos quatre
+ questions »). Platform shot: local fictional seed, learner view. LibreChat shot: drop an
+ anonymised capture at public/fb-s1/librechat.png; until then a labelled frame shows. */
+function Outil({ titre, texte, children }: { titre: string; texte: string; children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
{titre}
+
{texte}
+
+ );
+}
+
+function CaptureLibreChat() {
+ const [manque, setManque] = useState(false);
+ if (manque)
+ return (
+
+ capture LibreChat (compte anonymisé)
+
+ );
+ return setManque(true)} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transform: 'scale(1.3)', transformOrigin: '50% 42%' }} />;
+}
+
+export default [
+ /* s1-e05 */
+
+
+ La question n'est plus…
+
+ {/* the strike sits on the text box itself: one line, so 55 % is the x-height centre at any size */}
+
+ « Est-ce que l'IA nous concerne ? »
+
+
+
+
+
+
+ mais
+
+ « Où, chez nous, en premier ? »
+
+
+ ,
+
+ /* s1-e06 */
+
+ Avant qu'on se voie
+ Ce que vous m'avez dit
+
+ {[
+ { t: 'Votre profil', d: "Rempli lors de l'inscription à la plateforme : il me permet de guider toute la cohorte avec de bons exemples, adaptés à votre contexte." },
+ { t: 'Vos exercices', d: 'Ils guident chacune des séances, et la cohorte : pour que ce soit vraiment plus concret pour vous.' },
+ ].map((p, i) => (
+
+
+
+ ))}
+
+ ,
+
+ /* s1-outils — added in review, not yet in the brief */
+
+ Vos deux outils
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+
+ /* s1-e07 */
+
+
+
+
+
+
+
des Canadiens disent que l'IA les rend nerveux.
+
{SOURCE}
+
+
+
Vos quatre questions
+
+ {QUESTIONS.map((q, i) => (
+
+
+
+ ))}
+
+
+
+ ,
+
+ /* s1-e08 */
+
+ L'image à garder pour tout le parcours
+
+
+
+ Fiche d'employé
+ Poste : stagiaire
+
+
+
+ {HAS.map((t, i) => (
+
+
+
+ {t}
+
+
+ ))}
+
+
+ {LACKS.map((t, i) => (
+
+ ))}
+
+
+
+
+ ,
+];
diff --git a/src/decks/fb-s1/c03-magie.tsx b/src/decks/fb-s1/c03-magie.tsx
new file mode 100644
index 0000000..41972df
--- /dev/null
+++ b/src/decks/fb-s1/c03-magie.tsx
@@ -0,0 +1,113 @@
+import type { CSSProperties, ReactNode } from 'react';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import { Kicker, n } from './ui';
+
+/* Capsule « Vivre la magie… et le raté » — s1-e10 (s1-e09, the empty frames before the demo, removed in review). */
+
+function Frames({ left, right }: { left?: ReactNode; right?: ReactNode }) {
+ const frame = (label: string, content: ReactNode, tone: string, delay: number) => (
+
+ {label}
+
+ {content}
+
+
+ );
+ return (
+
+ {frame('Sur un document réel', left, 'var(--accent)', 0.1)}
+ {frame('Hors du document', right, 'var(--gold)', 0.22)}
+
+ );
+}
+
+/* s1-e10 — what went in, what came out: the document is why the magic worked, its absence why it missed. */
+const small: CSSProperties = { fontSize: 'clamp(15px, 1.5vw, 20px)', lineHeight: 1.35 };
+
+const Doc = ({ empty }: { empty?: boolean }) => (
+
+
+ {!empty && }
+
+);
+
+function Flow({ word, tone, soft, input, output, why }: { word: string; tone: string; soft: string; input: ReactNode; output: ReactNode; why: string }) {
+ const box: CSSProperties = { borderRadius: 12, padding: 'clamp(10px, 1.2vw, 16px)', display: 'grid', gap: 8, alignContent: 'center', justifyItems: 'center', textAlign: 'center', height: '100%' };
+ return (
+
+ );
+}
+
+const MAGIE = (
+ Un document réel du secteur >}
+ output={Cinq pistes classées et datées, puis un plan en trois étapes }
+ why="La connaissance était là : on la lui a donnée."
+ />
+);
+
+const RATE = (
+ Aucun document >}
+ output={Un contenu crédible , dit avec assurance… et faux }
+ why="Rien à lire : la connaissance est inventée."
+ />
+);
+
+export default [
+ /* s1-e10 */
+
+
+
+ Les deux faces de la même machine, avant toute théorie
+
+
+ L'enthousiasme est légitime, la vigilance aussi.
+
+
+ Personne n'oubliera la norme inventée.
+
+
+ ,
+];
diff --git a/src/decks/fb-s1/c04-generative.tsx b/src/decks/fb-s1/c04-generative.tsx
new file mode 100644
index 0000000..4795fde
--- /dev/null
+++ b/src/decks/fb-s1/c04-generative.tsx
@@ -0,0 +1,495 @@
+import { useState } from 'react';
+import type { CSSProperties, ReactNode } from 'react';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import { Kicker, card, source, n } from './ui';
+
+/* Capsule « IA générative vs IA traditionnelle » — s1-e11 to s1-e19. */
+
+const center: CSSProperties = { textAlign: 'center', marginInline: 'auto' };
+const h2: CSSProperties = { ...center, fontSize: 'clamp(28px, 3.8vw, 50px)', maxWidth: '26ch' };
+const body: CSSProperties = { fontSize: 'clamp(16px, 1.6vw, 22px)', lineHeight: 1.4 };
+const muted: CSSProperties = { ...body, color: 'var(--fg-muted)' };
+const grid = (min = 220, gap = 18): CSSProperties => ({
+ display: 'grid',
+ gridTemplateColumns: `repeat(auto-fit, minmax(min(${min}px, 100%), 1fr))`,
+ gap,
+ width: '100%',
+});
+const label: CSSProperties = {
+ fontFamily: 'var(--font-mono)',
+ fontSize: 'clamp(12px, 1.1vw, 15px)',
+ letterSpacing: '0.06em',
+ textTransform: 'uppercase',
+ color: 'var(--fg-muted)',
+};
+
+const Icon = ({ d }: { d: string }) => (
+
+
+
+);
+
+/* ---------- s1-e12: word bars stand for answers (no invented answer text) ---------- */
+
+const SAME = [5, 3, 6, 4, 7, 3, 5];
+const GEN = [
+ [5, 3, 6, 4, 7, 3, 5],
+ [5, 3, 6, 2, 4, 8, 3],
+ [5, 3, 4, 7, 3, 6, 2],
+ [5, 6, 3, 5, 2, 4, 7],
+];
+
+function Bars({ w, ref0 }: { w: number[]; ref0?: number[] }) {
+ return (
+
+ {w.map((x, i) => (
+
+
+
+ ))}
+
+ );
+}
+
+function Column({ title, sign, children }: { title: string; sign: string; children: ReactNode }) {
+ return (
+
+
+ {title}
+ {sign}
+
+ {children}
+
+ );
+}
+
+const MagieRate = ({ from }: { from: number }) => (
+
+
+
+
La magie · un bon document
+
Le plus plausible EST le bon.
+
+
+
+
+
Le raté · une norme qui n'existe pas
+
Le plus plausible n'est PAS le vrai.
+
+
+
+);
+
+function Redemander() {
+ const [k, setK] = useState(0);
+ return (
+
+
+
+
+
+
+
+
+
+
+ setK(k + 1)}
+ style={{
+ justifySelf: 'start',
+ padding: '10px 18px',
+ borderRadius: 999,
+ border: 'none',
+ background: 'var(--accent)',
+ color: 'var(--accent-ink)',
+ font: '600 clamp(14px, 1.3vw, 17px) var(--font-body)',
+ cursor: 'pointer',
+ }}
+ >
+ Redemander
+
+
+
+ );
+}
+
+/* ---------- s1-e13: skills crossing the human line (years from the screen text only) ---------- */
+
+const SRC12 = 'Source : Stanford AI Index 2026, p. 75–76 et 94 — scores ramenés à la performance humaine (100 %).';
+const PUNCH12 = 'Ce que les modèles savent, ils le savent de mieux en mieux. Ces examens ne disent rien de ce qu’ils inventent.';
+
+// ponytail: curve shapes are illustrative like the source SVG; only the crossing years are data.
+const HL = 200; // y of the human line
+const X1 = 760; // x of 2026
+const xOf = (year: number) => 60 + (year - 2012) * 50;
+const SKILLS = [
+ { what: 'Reconnaître une image', year: '2015', from: 2012, y0: 290, cross: 2015, end: 70 },
+ { what: 'Lire et répondre', year: '2019', from: 2016, y0: 280, cross: 2019, end: 100 },
+ { what: 'Niveau universitaire', year: '2024', from: 2020, y0: 290, cross: 2024, end: 130 },
+ { what: 'Science, niveau doctorat', year: 'fin 2024', from: 2022.5, y0: 310, cross: 2024.9, end: 160 },
+];
+const curve = (x0: number, y0: number, xc: number, yE: number) => {
+ const a = Math.min(30, (X1 - xc) / 2);
+ const k = (y0 - HL) * 0.35;
+ return `M${x0} ${y0} C${x0 + (xc - x0) * 0.45} ${y0} ${xc - a} ${HL + k} ${xc} ${HL} C${xc + a} ${HL - k} ${X1 - (X1 - xc) * 0.4} ${yE} ${X1} ${yE}`;
+};
+const t = (size: number, fill = 'var(--fg)', weight = 500): CSSProperties => ({ fontSize: size, fill, fontWeight: weight, fontFamily: 'var(--font-body)' });
+
+const SkillsChart = () => (
+
+
+
+ {[2012, 2015, 2018, 2021, 2024, 2026].map((y) => (
+ {y}
+ ))}
+
+ 100 %
+ humain
+ {SKILLS.map((s) => (
+
+ ))}
+ {SKILLS.map((s, i) => (
+
+
+
+ {s.year}
+
+ {s.what}
+
+ ))}
+ 2025 : 93 % contre 81 % (experts)
+ ENCORE SOUS LA LIGNE
+ Programmer seul de bout en bout
+ Se servir d’un ordinateur
+ comme vous
+
+);
+
+/* ---------- s1-e14: two stacked timelines, each proportional to time within itself ---------- */
+
+type D = [number, number, number];
+type Ev = { d: D; model: string; when: string; full: string };
+// ponytail: month-only dates in the screen text sit on the 1st of the month; ranges on their first day.
+const OLD: Ev[] = [
+ { d: [2025, 2, 1], model: 'Claude Code', when: 'févr.', full: "Des agents qui travaillent dans le terminal d'un développeur (Claude Code, février 2025)." },
+ { d: [2025, 4, 1], model: 'o3', when: 'avr.', full: 'Une nouvelle génération de modèles qui raisonnent avant de répondre (o3, avril 2025).' },
+ { d: [2025, 5, 1], model: 'Claude 4', when: 'mai', full: "Des modèles qui lisent des centaines de pages d'un coup (Claude 4, mai 2025)." },
+ { d: [2025, 8, 1], model: 'GPT-5', when: 'août', full: "Un modèle grand public qui choisit lui-même son niveau d'effort (GPT-5, août 2025)." },
+ { d: [2025, 9, 1], model: 'Sora 2', when: 'sept.', full: 'De la vidéo avec son et dialogue synchronisés (Sora 2, septembre 2025).' },
+ { d: [2025, 11, 1], model: 'Gemini 3, Opus 4.5', when: 'nov.', full: 'Des modèles qui pilotent un navigateur à votre place (Gemini 3 et Opus 4.5, novembre 2025).' },
+ { d: [2025, 12, 1], model: 'Cowork', when: 'déc.', full: "L'agent qui sort du terminal pour entrer dans l'application de bureau de tout le monde (Cowork, décembre 2025)." },
+ { d: [2026, 4, 1], model: 'Claude Mythos', when: 'avr.', full: 'Un modèle annoncé sans être mis en vente, d’abord réservé à des partenaires en cybersécurité défensive (Claude Mythos · Project Glasswing, avril 2026).' },
+];
+const NEW: Ev[] = [
+ { d: [2026, 6, 30], model: 'Sonnet 5', when: 'juin', full: 'Le prix d’un bon modèle s’effondre : 2 $ le million de jetons — les « mots » du modèle — en entrée (Claude Sonnet 5, 30 juin 2026).' },
+ { d: [2026, 7, 8], model: 'Grok 4.5, GPT-5.6', when: 'juil.', full: 'Deux grands modèles concurrents à un jour d’écart (Grok 4.5 le 8 juillet, GPT-5.6 le 9 juillet 2026).' },
+ { d: [2026, 8, 1], model: 'Qwen3.8-Max', when: 'août', full: 'Le plus gros modèle chinois devient installable chez soi, en accès libre (Qwen3.8-Max, août 2026).' },
+ { d: [2026, 8, 5], model: 'FLUX 3 Video', when: 'août', full: 'Vingt secondes de vidéo, son et dialogues synchronisés, ouverts à tous (FLUX 3 Video, 5 août 2026).' },
+ { d: [2026, 8, 6], model: 'GPT-5.6 Sol', when: 'août', full: 'Un curseur pour choisir : réfléchir plus longtemps, ou répondre plus vite (GPT-5.6 Sol, 6 août 2026).' },
+ { d: [2026, 8, 27], model: 'Nvidia + Hugging Face', when: 'août', full: 'Le fabricant de puces rachète la place publique des modèles ouverts, 12,9 milliards de dollars (Nvidia et Hugging Face, 27 août 2026).' },
+ { d: [2026, 9, 1], model: 'Quatre modèles', when: 'sept.', full: 'Quatre laboratoires sortent un modèle frontière en trois jours ; l’un dépasse l’humain sur 96 % des niveaux d’un test de raisonnement abstrait (Claude Fable 5.1, Gemini 3.8 Flash, Muse Spark 1.3, GPT-6 Astra, 1er–3 septembre 2026).' },
+ { d: [2026, 9, 3], model: 'K2 Horizon', when: 'sept.', full: 'Six modèles entièrement ouverts, publiés avec leur code et leurs données d’entraînement (K2 Horizon, 3 septembre 2026).' },
+];
+const day = ([y, m, d]: D) => Date.UTC(y, m - 1, d) / 864e5;
+const TOP: [number, number] = [day([2025, 1, 1]), day([2026, 9, 30])];
+const ZOOM: [number, number] = [day([2026, 6, 1]), day([2026, 9, 30])];
+const pct = (d: D, [a, b]: [number, number]) => ((day(d) - a) / (b - a)) * 100;
+const P_JUNE = pct([2026, 6, 1], TOP);
+const TITLE13 = 'Dix-huit mois, huit dates. Depuis juin, huit de plus.';
+const FOOT13 = 'Ce n’est pas une période exceptionnelle : c’est le rythme normal. Et il s’accélère.';
+
+const abs = (left: number, extra?: CSSProperties): CSSProperties => ({ position: 'absolute', left: `${left}%`, ...extra });
+const Dot = ({ at, color, r = 16, title }: { at: number; color: string; r?: number; title?: string }) => (
+
+);
+/** Axis line with month ticks above it. */
+function Axis({ range, ticks = [], below, children }: { range: [number, number]; ticks?: [D, string][]; below?: boolean; children: ReactNode }) {
+ return (
+
+
+ {ticks.map(([d, txt]) => (
+ {txt}
+ ))}
+ {children}
+
+ );
+}
+/** Labels in even slots over [0, width]%, joined to their real-date dot by a leader line. */
+function Labels({ events, range, width, color, below }: { events: Ev[]; range: [number, number]; width: number; color: string; below?: boolean }) {
+ const slot = (i: number) => (width * (i + 0.5)) / events.length;
+ const leaders = (
+
+ {events.map((e, i) => (
+
+ ))}
+
+ );
+ const row = (
+
+ {events.map((e) => (
+
+ {e.model}
+ {e.when}
+
+ ))}
+
+ );
+ return below ? <>{leaders}{row}> : <>{row}{leaders}>;
+}
+
+const Timelines = () => (
+
+
+
+ {OLD.map((e) => )}
+
+
+ {NEW.map((e) => )}
+
+
+
+
+
+
+
+
+
+ {NEW.map((e) => )}
+
+
+
+
+);
+
+/* ---------- s1-e15: the compilation, served from public/media/ (kept out of git) ---------- */
+
+function Compilation() {
+ const [ok, setOk] = useState(true);
+ const box: CSSProperties = { width: 'min(100%, calc(50vh * 16 / 9))', aspectRatio: '16 / 9', marginInline: 'auto', borderRadius: 'var(--radius)', overflow: 'hidden' };
+ return ok ? (
+ setOk(false)} style={{ ...box, display: 'block', background: 'var(--fg)' }} />
+ ) : (
+
+ vidéo : compilation Will Smith 2023–2026
+
+ );
+}
+
+
+/* ---------- slides ---------- */
+
+export default [
+ /* s1-e11 */
+
+
+
+
+ Vous l'utilisez depuis des années sans y penser.
+
+ {[
+ ['Détection de fraude sur votre carte', 'M3 6h18v12H3zM3 10h18M7 15h4'],
+ ['Itinéraire de votre GPS', 'M12 21s-6-6.2-6-11a6 6 0 0112 0c0 4.8-6 11-6 11zM12 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z'],
+ ['Recommandations de vos plateformes', 'M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1-4.4-4.3 6.1-.9z'],
+ ['Tri de vos pourriels', 'M3 6h18v12H3zM3 6l9 7 9-7'],
+ ].map(([t, d]) => (
+
+
+ {t}
+
+ ))}
+
+
+ ↓ ↓ ↓ ↓
+
+
+ IA traditionnelle
+
+
Entraînée pour UNE tâche, elle classe et prédit. Mêmes données, même réponse, toujours.
+
Fiable, spécialisée, invisible. Là depuis les années 1990.
+
+
+ ,
+
+ /* s1-e12 */
+
+ Elle ne récite pas : elle compose.
+
+
+
+
+ ,
+
+ /* s1-e13 */
+
+ Plus fort que nous, sur quoi ?
+ Année après année, une compétence de plus passe la ligne.
+
+
+
+
+ {PUNCH12}
+
+ {SRC12}
+ ,
+
+ /* s1-e14 */
+
+ {TITLE13}
+
+ ,
+
+ /* s1-e15 */
+
+ Le même prompt · mars 2023, 2025, 2026
+ « Will Smith mange des spaghettis »
+
+
+
+ Regardez.
+ ,
+
+ /* s1-e16 */
+
+ Trois nouveaux outils dans la trousse.
+
+ Déjà là :
+ {['Les automatisations', 'Les outils qui connectent deux applications', "L'IA prédictive"].map((t) => (
+ {t}
+ ))}
+
+
+ {[
+ ['Des assistants génératifs', 'qui rédigent, résument, proposent'],
+ ['Des flux de travail', "où l'IA s'insère dans une chaîne existante, pour structurer ou nettoyer ce qui passe"],
+ ['Des agents', 'qui enchaînent des actions et se contrôlent eux-mêmes'],
+ ].map(([t, d], i) => (
+
+ ))}
+
+ Leur nom précis : avec la carte des espèces. Leur place : avec l'échelle.
+ ,
+
+ /* s1-e17 */
+
+ Ce que font les entreprises AI-first
+
+
+
Duolingo
+ {[
+ { v: 148, t: 'nouveaux cours, en moins d’un an', c: 'var(--accent)' },
+ { v: 100, t: 'premiers cours, en une douzaine d’années', c: 'var(--fg-faint)' },
+ ].map((b) => (
+
+ ))}
+
+ Son fondateur : « Apporter des modifications mineures à des systèmes conçus pour les humains ne nous y mènera pas. Dans de nombreux cas, nous devrons repartir de zéro. »
+
+
+
+
Shopify
+
L'IA, une attente de base : avant de demander des embauches ou des ressources, une équipe démontre qu'elle ne peut pas faire le travail avec l'IA.
+
Il faut justifier de NE PAS l'utiliser.
+
+
+
+
+ Pas des entreprises de technologie qui vendent de l'IA : des organisations qui produisent , comme les vôtres.
+
+
+ ,
+
+ /* s1-e18 */
+
+ “
+
+ C'est parce qu'elle compose au lieu de réciter qu'elle peut écrire{' '}
+ VOTRE courriel, dans VOTRE contexte , d'une façon qui n'a jamais existé avant.
+
+
+ Ce non-déterminisme est le prix de sa souplesse. On ne corrige pas cette nature : on apprend à travailler avec.
+
+ ,
+
+ /* s1-e19 */
+
+ À retenir
+
+ {[
+ 'Traditionnelle = classe et prédit, toujours pareil. Générative = compose, jamais exactement pareil.',
+ 'La magie et le raté sont le même mécanisme : « le plus probable » n’est pas toujours « le vrai ».',
+ 'Le rythme des dix-huit derniers mois est le rythme normal : ce qu’on décide aujourd’hui vaut pour des outils qui auront changé dans six mois.',
+ 'C’est pour ça que votre consigne pèse autant. C’est tout l’objet de la séance 2.',
+ ].map((t, i) => {
+ const item = (
+
+ {i + 1}
+ {t}
+
+ );
+ return
{item} ;
+ })}
+
+ ,
+];
diff --git a/src/decks/fb-s1/c05-a-quoi-ca-sert.tsx b/src/decks/fb-s1/c05-a-quoi-ca-sert.tsx
new file mode 100644
index 0000000..156e295
--- /dev/null
+++ b/src/decks/fb-s1/c05-a-quoi-ca-sert.tsx
@@ -0,0 +1,385 @@
+import type { CSSProperties, ReactNode } from 'react';
+import { motion, useReducedMotion } from 'framer-motion';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import { useDeck } from '../../deck/DeckContext';
+import Prompt from '../../components/Prompt';
+import { Kicker, n } from './ui';
+
+/* Pause (s1-e20) + capsule « À quoi ça sert » (s1-e21 → s1-e24). */
+
+const center: CSSProperties = { textAlign: 'center', marginInline: 'auto' };
+const small: CSSProperties = { fontSize: 'clamp(14px, 1.3vw, 18px)', color: 'var(--fg-muted)', lineHeight: 1.45 };
+
+/* ── s1-e21 · six forms, drawn as plain stroke pictograms ─────────── */
+const FORMS: { label: string; d: string }[] = [
+ { label: 'Texte', d: 'M5 6h14M5 10h14M5 14h10M5 18h12' },
+ { label: 'Image', d: 'M4 5h16v14H4zM4 16l5-5 4 4 3-3 4 4M15.5 9.5h.01' },
+ { label: 'Code', d: 'M9 7l-5 5 5 5M15 7l5 5-5 5M13 5l-2 14' },
+ { label: '3D', d: 'M12 3l8 4.5v9L12 21l-8-4.5v-9zM12 12l8-4.5M12 12L4 7.5M12 12v9' },
+ { label: 'Audio', d: 'M4 12h2M8 8v8M12 4v16M16 8v8M20 11v2' },
+ { label: 'Vidéo', d: 'M3 6h13v12H3zM16 10l5-3v10l-5-3' },
+];
+
+function Pictogram({ d, lit, i }: { d: string; lit: boolean; i: number }) {
+ const { isStatic } = useDeck();
+ const reduce = useReducedMotion();
+ return (
+
+
+
+ );
+}
+
+/* ── s1-e23 · one line icon per family: pen, bar chart, magnifier ─── */
+const FAMILY_ICONS = [
+ ['Génération', 'M4 20l4-1 11-11-3-3L5 16zM14 7l3 3'],
+ ['Analyse', 'M4 20h16M7 16v-4M12 16V7M17 16v-7'],
+ ['Recherche', 'M10.5 17a6.5 6.5 0 100-13 6.5 6.5 0 000 13zM15.5 15.5L20 20'],
+] as const;
+const FamilyIcon = ({ d }: { d: string }) => (
+
+
+
+);
+
+/* ── s1-e22 · the three families, the screen's own wording ────────── */
+type Family = { title: string; short: string; prompt: string };
+const FAMILIES: Family[] = [
+ {
+ title: 'Générer et réécrire',
+ short: 'Produire un premier jet ou transformer un texte.',
+ prompt: 'Écris la réponse à ce client mécontent, ferme mais courtoise.',
+ },
+ {
+ title: 'Analyser, synthétiser, raisonner',
+ short: 'Digérer de la matière existante.',
+ prompt: 'Voici trois soumissions de fournisseurs : fais-moi un tableau comparatif.',
+ },
+ {
+ title: 'Chercher du savoir',
+ short: 'Se faire expliquer, apprendre, explorer — et vérifier tout fait précis.',
+ prompt: 'Explique-moi ce qu’est un profil colorimétrique, comme à quelqu’un qui débute.',
+ },
+];
+const COMBINE = (
+ <>
+ Les trois se combinent : analyser une demande, puis générer la réponse, en s’étant fait
+ expliquer le vocabulaire au passage.
+ >
+);
+
+function FamiliesHead() {
+ return (
+
+
+ Une notion : la famille d’usages
+
+
+ Trois familles d’usages
+
+
+ );
+}
+
+/* Venn of the three families; the last click traces analyser → chercher → générer. */
+function Venn() {
+ const { clicks, isStatic } = useDeck();
+ const traced = isStatic || clicks >= 1;
+ const circles = [
+ { cx: 150, cy: 120, label: 'Générer', color: 'var(--accent)' },
+ { cx: 250, cy: 120, label: 'Analyser', color: 'var(--gold)' },
+ { cx: 200, cy: 205, label: 'Chercher', color: 'var(--fg-muted)' },
+ ];
+ return (
+
+ {circles.map((c) => (
+
+ ))}
+ {circles.map((c, i) => (
+
+ {c.label}
+
+ ))}
+ {/* analyser the request → have the vocabulary explained → generate the answer */}
+
+
+
+ );
+}
+
+export default [
+ /* s1-e20 */
+
+
+
+ Pause
+
+
+ Caméras et micros peuvent rester fermés.
+
+ L’heure du retour est annoncée dans le clavardage.
+
+ ,
+
+ /* s1-e21 */
+
+
+
+
+
+ Six formes de contenu
+
+
+ {FORMS.map((f, i) => (
+
+ ))}
+
+
+ On travaille surtout le texte , là où se joue l’essentiel de votre
+ quotidien.
+
+
+
+ Les autres existent : vous en verrez à la séance 4 (présentations, transcription de réunions).
+
+
+ ,
+
+ /* s1-e22 */
+
+
+
+
+
+ {FAMILIES.map((f) => (
+
+
{f.title}
+
{f.short}
+
+ {f.prompt}
+
+
+ ))}
+
+
+
+
+ {COMBINE}
+
+
+ ,
+
+ /* s1-e23 */
+
+
+
+ Petite réflexion ensemble
+
+
+ Qui a une tâche en tête ? Une vraie, celle qui vous a occupé cette semaine. On la met à l’écran.
+
+
+ Génération, analyse, recherche — ou deux à la fois ?
+
+
+
+ {FAMILY_ICONS.map(([l, d]) => (
+
+
+
+ {l}
+
+
+ ))}
+
+
+ Vous faites à peu près le même métier : la tâche d’un seul parle à tout le monde.
+
+ ,
+
+ /* s1-e24 */
+
+
+
+ Le bon réflexe
+
+
+
+ Génération, analyse ou recherche ?
+ >
+ }
+ />
+
+ Si c’est l’une des trois, et c’est presque toujours le cas, la réponse est oui .
+
+
+
+ L’inventaire qui suit va le prouver sur vos tâches.
+
+
+ ,
+];
+
+/* « question » → « translated question », the arrow drawing itself once on enter. */
+function Translate({ from, to }: { from: string; to: ReactNode }) {
+ const { isStatic } = useDeck();
+ const reduce = useReducedMotion();
+ const still = isStatic || !!reduce;
+ const quote: CSSProperties = {
+ font: '600 clamp(22px, 3vw, 42px)/1.2 var(--font-head)',
+ maxWidth: '16ch',
+ textAlign: 'center',
+ };
+ return (
+
+
« {from} »
+
+
+
+
« {to} »
+
+ );
+}
diff --git a/src/decks/fb-s1/c06-especes-echelle.tsx b/src/decks/fb-s1/c06-especes-echelle.tsx
new file mode 100644
index 0000000..33dd4a6
--- /dev/null
+++ b/src/decks/fb-s1/c06-especes-echelle.tsx
@@ -0,0 +1,206 @@
+import type { CSSProperties } from 'react';
+import { motion, useReducedMotion } from 'framer-motion';
+import Slide from '../../deck/Slide';
+import Reveal from '../../deck/Reveal';
+import { useDeck } from '../../deck/DeckContext';
+import { Kicker, n } from './ui';
+import Escalier from './escalier';
+
+/* Capsules « La carte des espèces d'IA » (s1-e25) and « L'échelle » (s1-e26). */
+
+const center: CSSProperties = { textAlign: 'center', marginInline: 'auto' };
+const small: CSSProperties = { fontSize: 'clamp(13px, 1.2vw, 17px)', color: 'var(--fg-muted)', lineHeight: 1.4 };
+const ease = [0.16, 1, 0.3, 1] as const;
+
+/* ── s1-e25 · the species map (piste C retained) ──────────────────────
+ Centre = the whole parcours, ring = one séance each, dashed edge = glimpsed only. */
+type Species = { name: string; phrase: string; example: string; tag: string; kind: 'core' | 'peek' | 'seance' };
+const SPECIES: Species[] = [
+ { name: 'IA générative', phrase: 'Produit du contenu à la demande', example: 'ChatGPT qui rédige votre courriel', tag: 'Tout le parcours', kind: 'core' },
+ { name: 'IA prédictive', phrase: 'Classe et prédit à partir de données', example: 'La détection de fraude de votre banque', tag: 'Aperçu seulement', kind: 'peek' },
+ { name: 'Vision numérique', phrase: '« Voit » et interprète images et documents', example: 'L’outil qui lit une facture numérisée', tag: 'Séance 3 · fichiers', kind: 'seance' },
+ { name: 'Automatisation', phrase: 'Enchaîne des étapes selon des règles, sans IA', example: '« Quand un courriel arrive, classe-le dans ce dossier »', tag: 'Séance 5', kind: 'seance' },
+ { name: 'Copilote / assistant', phrase: 'Une IA configurée pour vous épauler dans UN contexte', example: 'Votre assistant « préparation de soumissions »', tag: 'Séance 3 · à bâtir', kind: 'seance' },
+ { name: 'Agent', phrase: 'Une IA qui agit : elle enchaîne des actions au lieu de seulement répondre', example: 'Il lit la demande, prépare le brouillon, crée le suivi', tag: 'Séance 5', kind: 'seance' },
+];
+const ISLANDS: { s: Species; x: number; y: number; w: number }[] = [
+ { s: SPECIES[0], x: 50, y: 47, w: 30 },
+ { s: SPECIES[2], x: 16, y: 20, w: 30 },
+ { s: SPECIES[4], x: 84, y: 20, w: 30 },
+ { s: SPECIES[3], x: 16, y: 76, w: 30 },
+ { s: SPECIES[5], x: 84, y: 76, w: 30 },
+ { s: SPECIES[1], x: 50, y: 11, w: 34 },
+];
+
+function useEnter(delay: number) {
+ const { isStatic } = useDeck();
+ const reduce = useReducedMotion();
+ return {
+ initial: isStatic || reduce ? false : { opacity: 0, scale: 0.94 },
+ animate: { opacity: 1, scale: 1 },
+ transition: { duration: 0.5, delay, ease },
+ } as const;
+}
+
+function Island({ s, x, y, w, i }: { s: Species; x: number; y: number; w: number; i: number }) {
+ const core = s.kind === 'core';
+ const peek = s.kind === 'peek';
+ return (
+
+
+ {s.tag}
+
+
+ {s.name}
+
+ {s.phrase}
+
+ {s.example}
+
+
+ );
+}
+
+/* Séance 4 is not a species: a landmark (a signpost on the outer ring), not an island.
+ Text only from the brief: « le retrouver dans vos outils », level 3 in demonstration,
+ other forms glimpsed (présentations, transcription de réunions). */
+function Seance4() {
+ return (
+
+
+ Repère · Séance 4
+
+ Le retrouver dans vos outils
+
+ L’assistant qui agit, en démonstration · autres formes : présentations, transcription de réunions
+
+
+ );
+}
+
+/* ── s1-e26 · the ladder (piste C retained, reworked) ─────────────────
+ An oblique-projection staircase drawn flat: fronts are untransformed (crisp text), lids and
+ sides are 2D skews painted in order, so there is no preserve-3d and nothing to z-fight.
+ The 3D is in the entrance: the picture swings in once (≤ 1 s) and ends on transform: none. */
+type Level = { n: number; name: string; what: string; where: string };
+const LEVELS: Level[] = [
+ { n: 0, name: 'Sans IA', what: 'Procédure, outil déjà payé, règle simple', where: 'Séance 5' },
+ { n: 1, name: 'Le chat', what: 'L’outil tel quel, vous relisez', where: 'Séances 1 et 2' },
+ { n: 2, name: 'L’assistant', what: 'Il connaît votre matière', where: 'Séances 3 et 4' },
+ { n: 3, name: 'L’assistant qui agit', what: 'Connecté à vos outils, il exécute', where: 'Séance 4, en démonstration' },
+ { n: 4, name: 'Le projet structurant', what: 'Processus revu, sur mesure', where: 'Séance 5' },
+];
+
+export default [
+ /* s1-e25 */
+
+
+
+
+ La carte des espèces d’IA
+
+
+
+ {[
+ [44, 'var(--accent-soft)', 'solid'],
+ [96, 'transparent', 'dashed'],
+ ].map(([size, bg, style]) => (
+
+ ))}
+ {ISLANDS.map((it, i) => (
+
+ ))}
+
+
+
+ « On devrait mettre de l’IA là-dessus » ? Première question : laquelle ? {' '}
+ Bien des déceptions viennent d’avoir pris une espèce pour une autre.
+
+ ,
+
+ /* s1-e26 */
+
+
+
+ Cinq niveaux, du chat au projet
+
+
+ L’échelle
+
+
+
+ {/* bottom margin keeps the rule clear of the deck's dock (fixed, bottom 22px) */}
+
+ On monte d’un niveau quand le précédent a prouvé quelque chose, jamais parce qu’il a déçu. À chaque idée, une seule
+ question : à quel niveau ça se joue ?
+
+ ,
+];
diff --git a/src/decks/fb-s1/c07-limites.tsx b/src/decks/fb-s1/c07-limites.tsx
new file mode 100644
index 0000000..bd718b8
--- /dev/null
+++ b/src/decks/fb-s1/c07-limites.tsx
@@ -0,0 +1,339 @@
+import { useState } from 'react';
+import type { CSSProperties, ReactNode } from 'react';
+import { motion, useReducedMotion } from 'framer-motion';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import { useInView } from '../../deck/useInView';
+import Accordion from '../../components/Accordion';
+import { Kicker, card, source, n } from './ui';
+
+/* Capsule « Les limites » — s1-e27 to s1-e31. */
+
+const head: CSSProperties = { fontSize: 'clamp(28px, 3.8vw, 52px)', maxWidth: '24ch' };
+const body: CSSProperties = { fontSize: 'clamp(17px, 1.7vw, 24px)', lineHeight: 1.45 };
+const center: CSSProperties = { textAlign: 'center', marginInline: 'auto' };
+
+/* s1-e27 — the three candidates and their probabilities, as the text gives them. */
+const P = [
+ { v: '7 ans', p: 0.42 },
+ { v: '5 ans', p: 0.31 },
+ { v: '10 ans', p: 0.18 },
+] as const;
+const fr = (p: number) => p.toFixed(2).replace('.', ',');
+
+const BEFORE = 'Ce matériau convient à un usage extérieur, et tient';
+const AFTER = 'avant de se dégrader.';
+
+function Sentence({ slot }: { slot: ReactNode }) {
+ return (
+
+ « {BEFORE} {slot} {AFTER} »
+
+ );
+}
+
+const Gap = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+);
+
+const Hallucination = () => (
+
+ Il lui manque une information ? Il la remplace par la version la plus probable , pas
+ la vraie. C'est une hallucination : rien, dans la phrase, ne
+ signale le trou. Le modèle ne ment pas : il complète.
+
+);
+
+/* « L'or aux Olympiades contre l'horloge en appui » */
+const Appui = () => (
+
+
+
+
L'or aux Olympiades de mathématiques
+
+
+
+ L'heure sur une horloge à aiguilles : une fois sur deux, et alors fausse d'une à trois heures
+
+
+
+
+ Le meilleur modèle de 2026 · Source : Stanford AI Index 2026, p. 71, 96 et 104.
+
+
+);
+
+/* « relancer » draws 7, 5 or 10 ans in the text's proportions (renormalised over the three). */
+function Tirage() {
+ const [last, setLast] = useState(P[0].v);
+ const [tally, setTally] = useState>({});
+ const relancer = () => {
+ const total = P.reduce((s, c) => s + c.p, 0);
+ let r = Math.random() * total;
+ const hit = P.find((c) => (r -= c.p) < 0) ?? P[0];
+ setLast(hit.v);
+ setTally((t) => ({ ...t, [hit.v]: (t[hit.v] ?? 0) + 1 }));
+ };
+ const max = Math.max(1, ...Object.values(tally));
+ return (
+ <>
+ {last}} />
+
+
+ Relancer
+
+ {P.map((c) => (
+
+
+ {c.v} · {fr(c.p)}
+
+
+
{tally[c.v] ?? 0} tirage(s)
+
+ ))}
+
+
+
+
+
+
+
+ >
+ );
+}
+
+/* s1-e28 — one big contrasted figure per case. */
+function Figure({ label, value, tone, bg }: { label: string; value: string; tone: string; bg: string }) {
+ return (
+
+
{label}
+
+ {value}
+ d’invention
+
+
+ );
+}
+
+const col = (title: ReactNode, items: string[], lit: boolean) => (
+
+
{title}
+
+ {items.map((i) => (
+ {i}
+ ))}
+
+
+);
+
+function Signature() {
+ const { ref, inView } = useInView(0.3);
+ const reduce = useReducedMotion();
+ return (
+
+
+
+
+ );
+}
+
+const RETENIR = [
+ "L'hallucination est normale et silencieuse : les faits que vous n'avez pas fournis se vérifient.",
+ 'Jamais de données clients ni de renseignements personnels dans un outil public.',
+ 'Vous signez, donc vous vérifiez. Ce temps-là reste, en général, plus court que tout faire à la main.',
+];
+
+export default [
+ /* s1-e27 */
+
+
+
+
+
+ ,
+
+ /* s1-e28 */
+
+
+
+ Où le risque est élevé, et où il est presque nul
+
+
+
+ {col('Risque élevé — l’IA comble ce qui manque', ['Les faits précis : chiffres, dates, noms, prix', 'Les références : normes, lois, articles, sources', 'Les calculs en série', "Tout ce qui n'était pas dans votre demande"], false)}
+ {col('Risque faible — la matière vient de vous', ['Reformuler ou résumer un texte que VOUS fournissez', 'Structurer, classer, faire un plan', 'Générer des variantes, brainstormer', 'Traduire, changer le ton'], true)}
+
+
+
+
+
+
+ Selon le modèle · Source : Stanford AI Index 2026, p. 136.
+
+
+
+ Plus la réponse contient de faits que vous n'avez pas fournis, plus elle se vérifie avant de servir.
+
+
+ ,
+
+ /* s1-e29 */
+
+
+
+ Ce que vous écrivez dans un outil d'IA public sort de chez vous . Avant de coller quoi que ce soit :
+
+
+ Serais-je à l'aise que ce texte se retrouve ailleurs ?
+
+
+
+ {[
+ ['✅', 'Public ou interne banal', 'texte de votre site, procédure générique : allez-y.', 'var(--accent-soft)'],
+ ['❌', 'Données de clients, chiffres sensibles, secrets d’affaires', 'non.', 'var(--gold-soft)'],
+ ['❌', 'Renseignements sur des personnes', 'noms, coordonnées, dossiers : jamais. C’est aussi la loi (Loi 25).', 'var(--gold-soft)'],
+ ].map(([icon, t, d, bg]) => (
+
+ {icon}
+
+ {t} — {d}
+
+
+ ))}
+
+
+ On y revient en profondeur à la séance 3, avec une grille de tri complète.
+ ,
+
+ /* s1-e30 */
+
+
+
+ Le résultat qui sort de votre bureau (courriel, soumission, rapport) engage{' '}
+ votre nom et votre entreprise , pas l'outil.
+
+
+ L'IA produit un brouillon ; vous restez l'auteur.
+
+
+
+
+
+ ,
+
+ /* s1-e31 */
+
+
+ À retenir
+
+
+ {RETENIR.map((t, i) => (
+
+
+
+ {i + 1}
+
+ {t}
+
+
+ ))}
+
+ ,
+];
diff --git a/src/decks/fb-s1/c08-inventaire.tsx b/src/decks/fb-s1/c08-inventaire.tsx
new file mode 100644
index 0000000..4cea6d8
--- /dev/null
+++ b/src/decks/fb-s1/c08-inventaire.tsx
@@ -0,0 +1,242 @@
+import type { CSSProperties, ReactNode } from 'react';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import { Kicker, card, source, n } from './ui';
+
+/* Capsule « L'inventaire des irritants » — s1-e32 to s1-e36.
+ Instruction screens stay on while the group works: big type, no motion past the builds. */
+
+const head: CSSProperties = { fontSize: 'clamp(28px, 3.8vw, 52px)', maxWidth: '26ch' };
+const body: CSSProperties = { fontSize: 'clamp(17px, 1.7vw, 24px)', lineHeight: 1.45 };
+const center: CSSProperties = { textAlign: 'center', marginInline: 'auto' };
+const FAMILLES = ['Générer', 'Analyser', 'Chercher'] as const;
+
+/* « l'inventaire du groupe, dessiné comme un document » */
+function Doc({ children, style }: { children: ReactNode; style?: CSSProperties }) {
+ return (
+
+ {children}
+
+ );
+}
+
+const Arrow = () => (
+
+ →
+
+);
+
+const COLS = [
+ ['Tâche', 'en une ligne, avec un verbe : « rédiger… », « comparer… », « retrouver… »'],
+ ['Famille', 'Générer · Analyser · Chercher'],
+ ['Gain espéré', 'du temps, moins d’oublis, un premier jet, une charge mentale en moins'],
+ ['Risque', 'ce qui fait mal si l’IA se trompe'],
+ ['Niveau estimé', '0 à 4, à l’instinct, d’après l’échelle'],
+] as const;
+
+const EXEMPLES = [
+ ['Rédiger la réponse à une demande de soumission pour des armoires de cuisine sur mesure, à partir des dimensions et de l’essence demandées', 'Générer', 'Un premier jet immédiat au lieu de la page blanche, à chaque demande reçue', 'Un prix, un délai ou une essence inventés ; un ton générique qui ne ressemble pas à l’entreprise', '1'],
+ ['Vérifier que le bon de commande et la liste de débit concordent avant de lancer la production', 'Analyser', 'Moins d’erreurs découvertes à l’atelier, une fois le bois coupé', 'Un écart manqué : le contrôle reste à faire par une personne', '1'],
+ ['Trouver dans les fiches techniques des fournisseurs le panneau ou le vernis qui convient à l’usage (humidité, intérieur ou extérieur)', 'Chercher', 'Moins de temps passé à fouiller les fiches pour une question de base', 'Spécification plausible mais fausse, à valider sur la fiche technique', '1'],
+] as const;
+
+const grid: CSSProperties = {
+ display: 'grid',
+ gridTemplateColumns: '1.4fr 0.8fr 1.2fr 1.2fr 0.7fr',
+ gap: 8,
+ textAlign: 'left',
+ fontSize: 'clamp(12px, 1.15vw, 16px)',
+ lineHeight: 1.35,
+};
+const cell: CSSProperties = { padding: '8px 10px', borderRadius: 8, background: 'var(--surface)', border: '1px solid var(--hair)' };
+
+const DESTINS = [
+ ['Séance 2 — le Promptuaire', 'Classé par tâche métier : les vôtres. L’avant/après se fait sur une ligne écrite ici.'],
+ ['Séance 3 — l’assistant spécialisé', 'Construit sur la tâche la plus citée du groupe.'],
+ ['Séance 5 — le cas d’affaires', 'L’inventaire ressort tel quel : le gain et le risque d’aujourd’hui deviennent deux des huit questions de la fiche.'],
+] as const;
+
+export default [
+ /* s1-e32 */
+
+
+
+
+ Un exercice chacun pour soi
+
+
+ Je vous montre comment le faire dans la plateforme.
+
+
+ ,
+
+ /* s1-e33 */
+
+
+ Une seule question pour tout l'exercice
+
+ Qu'est-ce qui vous prend du temps chaque semaine, sans rien vous apporter ?
+
+
+
+ Pas « où pourrait-on mettre de l'IA » : on part de l'irritant, pas de l'outil.
+
+
+ Les gains les plus nets : des tâches bien définies, qui reviennent souvent, et dont on voit vite si le résultat est bon. C'est ce qu'on cherche.
+
+ Source : Stanford AI Index 2026, p. 219.
+ ,
+
+ /* s1-e34 — example rows from content/mandats/formabois/exercices/inventaire-des-irritants.md. */
+
+
+ Voilà à peu près à quoi ressemblera le résultat. {' '}
+ Une ligne par tâche, visez 5 à 8 lignes , notez tout, même le banal.
+
+
+
+ {COLS.map(([t, d]) => (
+
+ ))}
+
+
+ Exemple, trois lignes fictives (secteur de la transformation du bois)
+ {EXEMPLES.map((r) => (
+
+ {r.map((c, i) => (
+
{c}
+ ))}
+
+ ))}
+
+
+
+
+ Trois lignes au niveau 1 : c'est normal. Le niveau monte quand la tâche exige vos documents (2) ou plusieurs outils enchaînés (3).
+ Une tâche hors des trois familles ? Gardez-la quand même.
+
+
+ ,
+
+ /* s1-e35 */
+
+
+ {FAMILLES.map((f) => (
+
+
+ {f}
+
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+
+ ))}
+
+
+ Les mêmes tâches reviennent d'une entreprise à l'autre.
+
+
+ On termine par la colonne risque : quels dangers reviennent, lesquels sont déjà couverts par ce qu'on a vu sur les hallucinations et la confidentialité.
+
+ ,
+
+ /* s1-e36 */
+
+
+
+ Résultat cible
+ L'inventaire du groupe
+
+ Des tâches réelles, classées par famille, avec le gain espéré et le risque. Déposé dans les Documents de la cohorte le jour même.
+
+
+ Vous repartez avec votre courte liste : un point de départ pour lundi matin.
+ Vos irritants communs, et ce que la suite du parcours vient régler.
+ Une banque de tâches réelles où les séances 2 à 5 puisent.
+
+
+
+ {DESTINS.map(([t, d], i) => (
+
+
+
+ ))}
+
+
+ Rappel : ce que vous donnez aujourd'hui nourrit les prochaines séances. {' '}
+ C'est l'intérêt de ces exercices, en plus d'apprendre la mécanique des IA.
+
+
+
+
+ ,
+];
diff --git a/src/decks/fb-s1/c09-cloture.tsx b/src/decks/fb-s1/c09-cloture.tsx
new file mode 100644
index 0000000..9986709
--- /dev/null
+++ b/src/decks/fb-s1/c09-cloture.tsx
@@ -0,0 +1,115 @@
+import type { CSSProperties } from 'react';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import { card, n } from './ui';
+import logoEts from './assets/logo-ets-formation.png';
+import logoMhosaic from './assets/logo.png';
+import logoFormabois from './assets/logo-formabois.png';
+
+/* Capsule « Clôture » — s1-e37, s1-e38. */
+
+const head: CSSProperties = { fontSize: 'clamp(28px, 3.8vw, 52px)', maxWidth: '26ch' };
+const body: CSSProperties = { fontSize: 'clamp(17px, 1.7vw, 24px)', lineHeight: 1.45 };
+const center: CSSProperties = { textAlign: 'center', marginInline: 'auto' };
+
+const GARDE = [
+ ['🗂️', 'Je consolide votre inventaire des irritants, avec ses niveaux estimés, et je le dépose dans vos Documents.'],
+ ['💡', 'Vous aurez accès au Promptuaire : des fiches où aller puiser, si vous le voulez.'],
+ ['📚', 'Dans Documents : des documents d’après séance pour aller plus loin, et le PDF de la séance d’aujourd’hui pour retrouver une information.'],
+ ['✍️', 'Avant de quitter : remplissez le feedback et l’évaluation sur la plateforme.'],
+] as const;
+
+/* Echo of s1-e09 / s1-e10: the two frames, filled with their word. */
+const Frame = ({ caption, word }: { caption: string; word: string }) => (
+
+
+ {word}
+
+
{caption}
+
+);
+
+export default [
+ /* s1-e37 */
+
+
+ Ce qu'on garde
+
+
+ {GARDE.map(([icon, t], i) => (
+
+ {icon}
+ {t}
+
+ ))}
+
+ ,
+
+ /* s1-e38 */
+
+
+
+
+
+ le prompt
+
+
+
+
+
+
+ La différence entre la magie et le raté ? Le prompt.
+
+
+ Ce n'est pas la chance qui décide : c'est ce qu'on lui écrit. Et ça s'apprend.
+
+
+
+
+ À la séance 2 , on démonte la consigne : pourquoi deux demandes qui « veulent dire la même chose »
+ ne donnent pas le même résultat, et comment écrire celle qui marche à tous les coups.
+
+
+
+ Merci.
+
+
+ ,
+];
diff --git a/src/decks/fb-s1/escalier.tsx b/src/decks/fb-s1/escalier.tsx
new file mode 100644
index 0000000..264983b
--- /dev/null
+++ b/src/decks/fb-s1/escalier.tsx
@@ -0,0 +1,86 @@
+import type { CSSProperties } from 'react';
+import { motion, useReducedMotion } from 'framer-motion';
+import { useDeck } from '../../deck/DeckContext';
+
+/* Five-step staircase drawn flat (oblique projection): fronts untransformed (crisp text), lids
+ and sides are 2D skews painted in order — no preserve-3d, nothing to z-fight. The 3D lives in
+ the entrance only (≤ 1 s, ends on transform: none). `lit` = the step in accent.
+ Used by « L'échelle » and « Le parcours ». */
+export type Marche = { n: number; name: string; what: string; where?: string };
+const ease = [0.16, 1, 0.3, 1] as const;
+const u = (k: number) => `calc(var(--u) * ${k})`;
+const W = 24; // step width (u)
+const D = 6; // drawn depth of lids and sides (u)
+const HU = [17, 21, 25, 29, 40]; // step heights (u); level 4 taller
+
+function Step({ l, i, top }: { l: Marche; i: number; top: boolean }) {
+ const front = top ? 'var(--accent)' : 'var(--surface)';
+ const lid = top ? 'color-mix(in srgb, var(--accent) 70%, var(--surface))' : 'var(--surface-2)';
+ const side = top ? 'color-mix(in srgb, var(--accent) 75%, var(--fg))' : 'color-mix(in srgb, var(--fg) 12%, var(--surface))';
+ const edge = top ? '1px solid var(--accent)' : '1px solid var(--hair)';
+ const face: CSSProperties = { position: 'absolute', boxSizing: 'border-box', border: edge };
+ return (
+
+ {/* lid: skewed to the upper right; the next (taller) step, painted later, covers what it should */}
+
+ {/* side: only the last step's stays visible */}
+
+ {/* front: flat, carries the text */}
+
+ {l.n}
+ {l.name}
+ {l.what}
+ {l.where}
+
+
+ );
+}
+
+export default function Escalier({ marches, lit }: { marches: Marche[]; lit: number }) {
+ const { isStatic } = useDeck();
+ const reduce = useReducedMotion();
+ const still = isStatic || !!reduce;
+ return (
+
+
+ {marches.map((l, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/decks/fb-s1/index.tsx b/src/decks/fb-s1/index.tsx
new file mode 100644
index 0000000..e47c97b
--- /dev/null
+++ b/src/decks/fb-s1/index.tsx
@@ -0,0 +1,20 @@
+import Deck from '../../deck/Deck';
+import './theme.css';
+import c01 from './c01-ouverture';
+import c02 from './c02-pourquoi';
+import c03 from './c03-magie';
+import c04 from './c04-generative';
+import c05 from './c05-a-quoi-ca-sert';
+import c06 from './c06-especes-echelle';
+import c07 from './c07-limites';
+import c08 from './c08-inventaire';
+import c09 from './c09-cloture';
+
+/* Séance 1 — Formabois (L'Établi de l'IA). Built from
+ app-formation-ai conception/generes/mandats/formabois/seances/seance-1/brief.md
+ (storyboard D122). One file per capsule, each exporting its slides in order. */
+document.title = 'Séance 1 — Formabois';
+
+export default function FbS1() {
+ return {[...c01, ...c02, ...c03, ...c04, ...c05, ...c06, ...c07, ...c08, ...c09]} ;
+}
diff --git a/src/decks/fb-s1/revue.json b/src/decks/fb-s1/revue.json
new file mode 100644
index 0000000..009b922
--- /dev/null
+++ b/src/decks/fb-s1/revue.json
@@ -0,0 +1,121 @@
+{
+ "s1-e02": {
+ "slide": 2,
+ "decision": "modifier",
+ "note": "Ici, pas besoin d'animation sur le chiffre un, deux et trois à chaque clic.",
+ "fait": true,
+ "maj": "2026-10-05T22:18:19.318Z"
+ },
+ "s1-e04": {
+ "slide": 4,
+ "decision": "ok",
+ "note": "\n",
+ "fait": true,
+ "maj": "2026-10-05T22:26:28.156Z"
+ },
+ "s1-e05": {
+ "slide": 5,
+ "decision": "modifier",
+ "note": "J'ai l'impression que la phrase est-ce que l'ia nous concerne devrait être barrée par la ligne marron qui s'affiche, mais elle est légèrement décalée en dessous, donc l'effet ne rend pas très bien corrige ça.",
+ "fait": true,
+ "maj": "2026-10-05T22:26:22.885Z"
+ },
+ "s1-e03": {
+ "slide": 3,
+ "decision": "modifier",
+ "note": "Alors c'est sympa comme ça, mais ça ressemble trop à la slide qu'utilise justement le l'escalier en trois D. Donc je veux que tu me proposes quelque chose de dans la même idée mais pas forcément le même design exactement que la slide qui est déjà utilisée.",
+ "fait": true,
+ "maj": "2026-10-05T22:27:11.139Z"
+ },
+ "s1-e06": {
+ "slide": 6,
+ "decision": "modifier",
+ "note": "Ici, je j'aime pas trop commencer présenter, je préférais tout simplement qu'on dise que vous avez rempli votre profil lors de l'inscription à la plateforme, ça, ça va me permettre de guider l'ensemble de la cohorte avec des bons exemples au contexte et même chose pour l'ensemble des exercices, ils vont pour but de guider chacune des séances et la cohorte pour être vraiment plus concret pour vous.",
+ "fait": true,
+ "maj": "2026-10-05T22:29:16.084Z"
+ },
+ "s1-e10": {
+ "slide": 10,
+ "decision": "modifier",
+ "note": "C'est bien, mais je préférais que tu enlèves « Que dit la norme CSA Z-417 ? »\n\nPuis ça permettra aux deux blocs de faire la même taille.",
+ "fait": true,
+ "maj": "2026-10-05T22:30:41.642Z"
+ },
+ "s1-e14": {
+ "slide": 14,
+ "decision": "modifier",
+ "note": "j'adore, c'est trop bien, par contre, j'aimerais que tu donnes un petit peu plus d'espace parce que c'est un petit peu trop resserré, ça pourrait respirer un peu plus, c'est un petit peu trop chargé.",
+ "fait": true,
+ "maj": "2026-10-05T22:31:13.221Z"
+ },
+ "s1-e20": {
+ "slide": 20,
+ "decision": "ok",
+ "note": "J'aimerais juste que tu me rajoutes le mot pose en plus grand tout simplement.\n",
+ "fait": true,
+ "maj": "2026-10-05T22:32:18.087Z"
+ },
+ "s1-e22": {
+ "slide": 22,
+ "decision": "ok",
+ "note": "",
+ "fait": true,
+ "maj": "2026-10-05T22:33:36.174Z"
+ },
+ "s1-e23": {
+ "slide": 23,
+ "decision": "ok",
+ "note": "J'aimerais que tu rajoutes des icônes pour décrire un petit peu chacune des trois familles.",
+ "fait": true,
+ "maj": "2026-10-05T22:33:40.007Z"
+ },
+ "s1-e28": {
+ "slide": 28,
+ "decision": "modifier",
+ "note": "J'aime les deux blocs qui expliquent les risques élevés et les risques faibles, mais par contre les deux schémas en dessous avec les pourcentages trouvent que c'est pas très clair. J'aimerais que tu me refasses cette section là qui est quelque chose de plus clair.",
+ "fait": true,
+ "maj": "2026-10-05T22:35:13.103Z"
+ },
+ "s1-e32": {
+ "slide": 32,
+ "decision": "modifier",
+ "note": "Ici, j'aimerais tout simplement que cet annonce que c'est un exercice qui va être fait chacun pour soi et que je vais présenter comment il doit être fait dans la plateforme.",
+ "fait": true,
+ "maj": "2026-10-05T22:36:56.381Z"
+ },
+ "s1-e34": {
+ "slide": 34,
+ "decision": "ok",
+ "note": "Tu peux enlever \"Ouvrez votre agenda ou vos courriels des deux dernières semaines : la mémoire ment, l'agenda non\"\n\nIci, je dirais plutôt voilà à quoi ressemblera à peu près les le résultat de l'exercice.\nIdéalement, il faudrait prendre un screenshot d'a quoi ressemble la plateforme lorsque le l'exercice est rempli, mais on n'est pas obligé.",
+ "fait": true,
+ "maj": "2026-10-05T22:39:17.922Z"
+ },
+ "s1-e35": {
+ "slide": 35,
+ "decision": "modifier",
+ "note": "Enlève le \"En plénière\".\nIci, pour le moment, on va la garder un peu plus simplement, on va enlever la première phrase et le en plénière, on va garder le reste parce que, dans le futur, ce sera connecté au résultat de l'analyse une fois que tous les apprenants ont donné leur résultat.",
+ "fait": true,
+ "maj": "2026-10-05T22:44:43.079Z"
+ },
+ "s1-e36": {
+ "slide": 36,
+ "decision": "modifier",
+ "note": "Ici, je veux que tu mentionnes que c'est un rappel qui explique que les informations qui donnent actuellement, c'est des informations qui vont être utiles pour les futures séances qui vont être rattachables au plus au pro future séance et que c'est ça l'intérêt un petit peu de de faire ces exercices en plus de leur apprendre comment la mécanique se fait pour utiliser les IA aujourd'hui.",
+ "fait": true,
+ "maj": "2026-10-05T22:45:36.194Z"
+ },
+ "s1-e37": {
+ "slide": 37,
+ "decision": "modifier",
+ "note": "Enlève la ligne sur le Cheat Sheet\nEnlève la troisième ligne sur le vocabulaire.\nPour la quatrième ligne, juste annonce leur que ils vont avoir accès au promptuaire et qu'il y aura donc des informations qui pourront aller récolter s'ils levent.\nPour la cinquième ligne avant de quitter donc dir leur qui a le feedback et l'évaluation qui doit être rempli dans la plateforme.\nJ'aimerais que tu rajoutes une ligne pour dire qu'il y aura des documents d'après séance pour aller plus loin qui vont être disponibles sur la plateforme et qui se trouveront en document ainsi que un P D F de la séance qu'on a vu aujourd'hui s'ils veulent retrouver des informations. Supprime \"La séance 2 s’ouvre sur un cas de votre plancher : une demande de soumission à décortiquer, et la consigne qui fait la différence.\"",
+ "fait": true,
+ "maj": "2026-10-05T22:48:52.752Z"
+ },
+ "s1-e09": {
+ "slide": 9,
+ "decision": "retirer",
+ "note": "",
+ "fait": true,
+ "maj": "2026-10-05T23:20:47.178Z"
+ }
+}
diff --git a/src/decks/fb-s1/theme.css b/src/decks/fb-s1/theme.css
new file mode 100644
index 0000000..c6c0ee2
--- /dev/null
+++ b/src/decks/fb-s1/theme.css
@@ -0,0 +1,30 @@
+@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@500;600;700&family=Instrument+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
+
+/* Formabois S1 — light theme from content/mandats/formabois/profil.md.
+ Loaded only with this deck (lazy), so it overrides the starter tokens. */
+html { color-scheme: light; }
+:root {
+ --bg: #F8F7F5;
+ --bg-grad-1: rgba(67, 56, 202, 0.05);
+ --bg-grad-2: rgba(138, 99, 32, 0.04);
+ --surface: #FFFFFF;
+ --surface-2: #F1F0EC;
+ --fg: #1D1D22;
+ --fg-muted: #6B6B74;
+ --fg-faint: #9A9AA2;
+ --hair: #E3E1DB;
+ --hair-2: #EEECE7;
+ --primary: #4338CA;
+ --accent: #4338CA;
+ --accent-ink: #FFFFFF;
+ --accent-soft: #EEF0FB;
+ --gold: #8A6320;
+ --gold-soft: #F4EBD8;
+ --shadow: 0 30px 70px -40px rgba(29, 29, 34, 0.25);
+ --glow: 0 20px 50px -30px rgba(67, 56, 202, 0.3);
+ --grain: 0;
+ --vignette: transparent;
+ --font-head: 'Bricolage Grotesque', system-ui, sans-serif;
+ --font-body: 'Instrument Sans', system-ui, sans-serif;
+ --font-mono: 'IBM Plex Mono', ui-monospace, monospace;
+}
diff --git a/src/decks/fb-s1/ui.tsx b/src/decks/fb-s1/ui.tsx
new file mode 100644
index 0000000..65e5080
--- /dev/null
+++ b/src/decks/fb-s1/ui.tsx
@@ -0,0 +1,56 @@
+import type { CSSProperties, ReactNode } from 'react';
+
+/* Shared pieces of the Formabois S1 deck. Capsule files import from here so
+ every screen speaks the same visual language. */
+
+/** The day's path (s1-e04 « Le chemin d'aujourd'hui »), no durations on screen (D115). */
+export const JOUR = [
+ 'Accueil',
+ 'Pourquoi vous êtes ici',
+ 'La magie et le raté',
+ 'Générative vs traditionnelle',
+ 'À quoi ça sert',
+ 'Espèces et échelle',
+ 'Les limites',
+ "L'inventaire des irritants",
+] as const;
+
+/** Surtitre of a capsule's first screen: capsule name + the day's path, current step lit
+ (deck-wide rule of the storyboard: capsule title screens are out). */
+export function Kicker({ step, children }: { step: number; children?: ReactNode }) {
+ return (
+
+
+ {JOUR.map((_, i) => (
+
+ ))}
+
+
{children ?? JOUR[step]}
+
+ );
+}
+
+export const card: CSSProperties = {
+ background: 'var(--surface)',
+ border: '1px solid var(--hair)',
+ borderRadius: 'var(--radius)',
+ padding: 'clamp(16px, 2vw, 26px)',
+ textAlign: 'left',
+};
+
+export const source: CSSProperties = {
+ fontSize: 'clamp(11px, 1vw, 13px)',
+ color: 'var(--fg-faint)',
+ marginTop: 'clamp(12px, 2vh, 24px)',
+};
+
+/** Facilitator notes: bullets → the presenter's text. */
+export const n = (...bullets: string[]) => bullets.map((b) => '• ' + b).join('\n');
diff --git a/src/decks/s2-gc/index.tsx b/src/decks/s2-gc/index.tsx
new file mode 100644
index 0000000..e31a03f
--- /dev/null
+++ b/src/decks/s2-gc/index.tsx
@@ -0,0 +1,1231 @@
+import type { CSSProperties, ReactNode } from 'react';
+import Deck from '../../deck/Deck';
+import Slide from '../../deck/Slide';
+import Build from '../../deck/Build';
+import Reveal from '../../deck/Reveal';
+import Cover from '../../components/Cover';
+import Steps from '../../components/Steps';
+import Table from '../../components/Table';
+import Timeline from '../../components/Timeline';
+import Prompt from '../../components/Prompt';
+
+/* Séance 2 — GrafiCompétences (L'Établi de l'IA). PREVIEW v1.
+ Sources: content/ bricks of séance 2, order and cuts per D116–D118.
+ Facilitator notes live in `notes` (presenter mode, P). */
+
+const n = (...bullets: string[]) => bullets.map((b) => '• ' + b).join('\n');
+
+const card: CSSProperties = {
+ padding: 'clamp(14px, 1.8vw, 22px)',
+ borderRadius: 'var(--radius)',
+ textAlign: 'left',
+};
+const small: CSSProperties = {
+ fontSize: 'clamp(13px, 1.25vw, 17px)',
+ color: 'var(--fg-muted)',
+ lineHeight: 1.45,
+};
+const h3: CSSProperties = {
+ fontSize: 'clamp(17px, 1.7vw, 23px)',
+ marginBottom: 8,
+};
+const gap = (g = 20): CSSProperties => ({ marginTop: g });
+const stack: CSSProperties = { display: 'grid', gap: 10, alignContent: 'start' };
+
+function Head({
+ k,
+ title,
+ sub,
+ center,
+}: {
+ k?: string;
+ title: ReactNode;
+ sub?: ReactNode;
+ center?: boolean;
+}) {
+ const c: CSSProperties = center
+ ? { textAlign: 'center', marginInline: 'auto' }
+ : {};
+ return (
+
+ {k && (
+
+ {k}
+
+ )}
+
+ {title}
+
+ {sub && (
+
+ {sub}
+
+ )}
+
+ );
+}
+
+function Card({
+ title,
+ children,
+ style,
+}: {
+ title?: ReactNode;
+ children?: ReactNode;
+ style?: CSSProperties;
+}) {
+ return (
+
+ {title &&
{title} }
+ {children &&
{children}
}
+
+ );
+}
+
+const SIX = [
+ ['Objectif', "Qu'est-ce que je veux obtenir, concrètement ?"],
+ ['Contexte', 'Que doit-il savoir (mon rôle, la situation, le destinataire) ?'],
+ ['Sources', "Sur quoi doit-il s'appuyer (document, données, exemple) ?"],
+ ['Contraintes', 'Que doit-il respecter — et éviter ?'],
+ ['Format', 'Quelle forme pour la sortie (liste, tableau, courriel, longueur) ?'],
+ ['Critères de qualité', 'À quoi reconnaîtrai-je que c’est bon ?'],
+];
+
+function Situation({
+ n: num,
+ title,
+ setup,
+ before,
+ missing,
+ after,
+ takeaway,
+}: {
+ n: number;
+ title: string;
+ setup: string;
+ before: string;
+ missing: string[];
+ after: string;
+ takeaway: string;
+ nav?: string;
+ notes?: string;
+}) {
+ return (
+
+
+
+
+
{setup}
+
+ {before}
+
+
+ Il manquait :
+ {missing.map((m) => (
+
+ {m}
+
+ ))}
+
+
+ {takeaway}
+
+
+
+ {after}
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+
+ {/* ── Ouverture ─────────────────────────────────────────── */}
+
+ Utiliser efficacement{' '}
+ les modèles de langage
+ >
+ }
+ subtitle="Lire un prompt, le construire, dialoguer avec le modèle — sur vos vraies tâches."
+ foot="En direct sur Zoom · 3 h 30"
+ notes={n(
+ 'Accueil 15 min : présence, LibreChat, tour de micro.',
+ 'Vérifier la veille que LibreChat répond (GPT-5.5 ; les modèles Claude répondaient 403 le 3 oct).'
+ )}
+ />
+
+
+
+
+
+
+ Le mauvais prompt, ensemble.
+ Comment il lit, les six cases, l'ordre, l'exemple.
+ Continuer la conversation, faire critiquer, la boucle.
+ Trois situations, les images IA floues.
+ Avant/après en sous-groupes, puis le Promptuaire.
+
+
+ Gardez sous la main votre tâche réelle — celle de
+ l'inventaire de la séance 1.
+
+
+
+ {/* ── Vivre le mauvais prompt (brique 2) ───────────────── */}
+
+
+
+
Écris un courriel de suivi à un client.
+
+
+
+ À main levée : l'enverriez-vous telle quelle , aujourd'hui,
+ à un vrai client ?
+
+
+
+
+
+
+
+
+ {`Objet : Suivi de notre échange
+
+Bonjour [Nom du client],
+
+Je fais suite à notre échange concernant [projet]. N'hésitez pas à me faire part de vos questions.
+
+Cordialement,
+[Votre nom]`}
+
+
+ {`Bonjour,
+
+J'espère que vous allez bien ! Je souhaitais prendre de vos nouvelles et m'assurer que tout se passe bien de votre côté. Au plaisir de poursuivre notre belle collaboration.`}
+
+
+ {`Avec plaisir ! Pouvez-vous me préciser le client, l'objet du suivi et le ton souhaité ?`}
+
+
+
+
+ Aucune ne contient un seul fait vrai sur votre client.
+
+
+
+
+
+
+ Qu'est-ce qui manquait ?
+ >
+ }
+ center
+ />
+
+ Chacun, au micro, nomme une information que le modèle aurait
+ eu besoin de savoir.
+
+
+ Je note tout, en vrac, sur mon écran.
+
+
+
+ {/* ── Comment un LLM lit votre prompt (brique 3) ───────── */}
+
+
+ Il lit tout. Il ne devine rien.
+ >
+ }
+ center
+ />
+
+
+
+ Votre message, les documents fournis, l'historique. Pas votre intention,
+ pas votre contexte d'entreprise, pas « ce qui va de soi ».
+
+
+
+
+ À partir du plus probable. Une réponse générique est presque toujours le
+ symptôme d'un prompt qui ne disait pas assez.
+
+
+
+
+
+ Dire = obtenir. Avant de blâmer l'outil, relisez ce que vous lui avez
+ réellement donné.
+
+
+
+
+ {/* ── L'anatomie d'un prompt (brique 4) ─────────────────── */}
+
+
+
+ {SIX.map(([c, q], i) => (
+
+ {q}
+
+ ))}
+
+
+
+
+
+
+
+
+ Pas un sujet, une commande.
+
+
+ Aide-moi avec le devis du client Tremblay.
+
+
+ Rédige le courriel qui accompagne le devis du client Tremblay.
+
+
+
+
+ La case qui transforme un texte passe-partout en votre texte.
+
+
+ Je suis chargée de projet dans un atelier d'impression d'une douzaine de
+ personnes. Le client est un organisme communautaire qui commande chez nous
+ pour la première fois, et le budget est serré.
+
+
+
+
+ Le contexte se décrit , la source se colle .
+
+
+ Voici le devis en question : […]. Voici aussi un courriel du même type que
+ j'ai envoyé le mois dernier et qui a bien fonctionné : […].
+
+
+
+
+
+
+
+
+
+
+ La moitié « éviter » est presque toujours oubliée.
+
+
+ 150 mots maximum. Aucun terme technique de prépresse. N'annonce aucun délai
+ de livraison : ils ne sont pas confirmés.
+
+
+
+
+ Un texte utilisable tel quel, pas un pavé à découper.
+
+
+ Un courriel : une ligne d'objet, deux courts paragraphes, une liste de trois
+ points pour les options, une phrase de clôture.
+
+
+
+
+ Le modèle reçoit sa propre grille de correction.
+
+
+ C'est réussi si le client comprend ce qu'il paie sans avoir besoin de me
+ rappeler.
+
+
+
+
+
+
+
+
+
+
+ Écris une publication LinkedIn sur l'impression grand format.
+
+
+ Un texte lisse sur l'impression grand format en général. Rien qui vous
+ appartienne.
+
+
+
+
+
+ Écris une publication LinkedIn pour annoncer que notre atelier lance un
+ service d'impression grand format.
+
+
+ Enfin une annonce, mais interchangeable : n'importe quel atelier du Québec
+ pourrait la publier.
+
+
+
+
+
+
+
+
+
+
+
+ Même demande. Contexte : atelier d'impression d'une douzaine de personnes,
+ région de Québec ; nos clients sont des chargés de marketing de PME qui
+ n'ont pas de designer à l'interne. Contraintes : 120 mots maximum, aucun
+ superlatif, ne promets aucun délai.
+
+
+ Le texte s'adresse enfin à quelqu'un ; les promesses vagues disparaissent.
+
+
+
+
+
+ Même demande, mêmes contraintes. Voici la fiche technique du service :
+ […]. Voici une de nos publications qui a bien fonctionné, pour le ton :
+ […]. Format : une accroche d'une ligne, trois puces (ce que ça permet,
+ pour qui, comment nous joindre), une question de clôture. C'est réussi si
+ un chargé de marketing comprend en dix secondes s'il est concerné.
+
+
+ Vos vraies capacités, votre ton, la bonne forme : il reste une relecture.
+
+
+
+
+
+
+ Entre la version 1 et la version 4, le modèle n'a pas changé.{' '}
+ Vous, oui.
+
+
+
+
+ {/* ── Ordre, clarté, contradictions (brique 5) ─────────── */}
+
+
+
+
+
+ Coller trois pages de brief, puis écrire à la fin « peux-tu m'aider avec
+ ça ? ». Le modèle reçoit surtout un document… et il le résume.
+
+
+ La consigne en tête, la matière encadrée au milieu, la contrainte qui
+ compte vraiment rappelée à la fin.
+
+
+
+ {`Rédige la réponse au brief ci-dessous, en 200 mots, pour un client sans culture graphique.
+
+— DÉBUT DU BRIEF — […] — FIN DU BRIEF —
+
+Rappel : 200 mots, aucun jargon.`}
+
+
+
+
+
+
+
+
+
+ Une ambiguïté n'est jamais signalée : elle est tranchée en silence. Une
+ phrase, une intention.
+
+
+ Fais un truc court pour le client.
+
+
+ Réécris ce texte de 250 mots en 90 mots pour le verso d'une carte postale
+ 5 × 7 po, sans retirer les coordonnées ni changer les faits.
+
+
+
+
+ « Sois exhaustif mais reste bref » donne un compromis mou. Ne supprimez pas
+ la tension : dites laquelle l'emporte.
+
+
+ Priorité à la brièveté : 120 mots maximum. Si tu dois couper, coupe les
+ détails techniques avant les coordonnées.
+
+
+
+
+
+ {/* ── Montrez-lui un exemple (brique 6) ─────────────────── */}
+
+
+
+
+ Écris un courriel de relance à un client. Ton professionnel mais chaleureux,
+ dynamique sans être insistant, concis mais complet.
+
+
+ {`Voici un courriel de relance que j'ai envoyé le mois dernier et qui a bien fonctionné :
+
+[collez le courriel au complet]
+
+Écris le prochain, dans le même ton et la même structure, pour le dossier suivant : […].`}
+
+
+
+
+ Pour trier des demandes, montrez deux ou trois cas déjà classés.
+
+
+ On le colle, on ne le décrit pas. Un contre-exemple marche aussi.
+
+
+
+
+ {/* ── Le prompt comme conversation (brique 7) ───────────── */}
+
+
+ On ne recommence pas : on continue.
+ >
+ }
+ sub="La première réponse est un premier jet. La conversation contient déjà votre contexte."
+ center
+ />
+
+
+
Ajouter la case qui manquait.
+
+ Reprends ta réponse. Le destinataire est le propriétaire de l'entreprise,
+ pas son équipe marketing.
+
+
+
+
Resserrer ce qui a débordé.
+
+ Trop long et trop enthousiaste. Refais-le en 120 mots, sans superlatif, en
+ gardant les trois options.
+
+
+
+
Demander une variante.
+
+ Donne-moi deux autres versions de l'accroche : une plus directe, une plus
+ formelle.
+
+
+
+
+
+
+
+
+
+ Une conversation qui a servi à un devis traîne son vocabulaire.
+
+
+ Il tourne en rond ou se contredit : repartez avec un prompt propre, enrichi de
+ ce que vous avez appris.
+
+
+
+
+ Arrivé à la bonne sortie ? Le chemin parcouru est la version réutilisable de
+ votre prompt : une entrée pour votre Promptuaire .
+
+
+
+
+ {/* ── Pause ─────────────────────────────────────────────── */}
+
+ Pause
+
+ 10 min
+
+
+ On reprend avec la critique.
+
+
+
+ {/* ── Faire critiquer sa propre sortie (brique 8) ───────── */}
+
+
+
+
+
+
+ Relis ta réponse. Qu'est-ce qui lui manque pour être utilisable telle quelle
+ par [le destinataire] ? Liste les manques, ne réécris rien pour l'instant.
+
+
+
+
+
+ Donne-moi les trois faiblesses les plus sérieuses de ce texte, du point de
+ vue de [le destinataire]. Puis produis une version corrigée qui les règle.
+
+
+
+
+
+ Évalue ce texte sur 10 selon ces critères : [vos critères]. Justifie la note
+ critère par critère, puis écris la version qui obtiendrait 10.
+
+
+
+
+
+
+
+ Un deuxième avis, pas un audit
+ >
+ }
+ sub="La critique sort de la même machine : utile pour un oubli ou une lourdeur, incapable de dire qu'un prix est faux. Les faits, les chiffres et les engagements restent à votre œil de métier."
+ center
+ />
+
+
+
+ « On pourrait ajouter un mot de transition. »
+
+
+ Prenez la version que vous aviez deux tours plus tôt.
+
+
+
+
+
+ {/* ── La boucle, version courte (brique 15) ─────────────── */}
+
+
+ {/* ── Trois situations (brique 9, GC) ──────────────────── */}
+
+
+
+ +50 %}>
+ de publicités produites par personne, dans des équipes marketing, grâce à
+ l'IA.
+
+ 17 %}>
+ des tâches en arts, design et médias suscitent l'envie de les confier à l'IA
+ : les créatifs sont les moins pressés.
+
+
+
+ Source : Stanford AI Index 2026, p. 219 et 226.
+
+
+
+
+
+
+
+
+
+ {/* ── Les images IA floues (brique 11, GC) ──────────────── */}
+
+
+
+
Le diagnostic en trente secondes
+
+ taille (cm) = pixels ÷ ppp × 2,54
+
+
+ 1024 px à 300 ppp → 8,7 cm (petite étiquette) · à 150 ppp,
+ grand format vu de loin → 17 cm
+
+
+
+
+
+
+
+
+
+ Premier réflexe : « avez-vous gardé votre prompt ? »
+
+
+
+
+
+
+
+
+ Décris ce logo pour qu'un graphiste puisse le redessiner à l'identique : les
+ formes et leur disposition, chaque couleur avec son code hexadécimal, le style
+ (plat, dégradé, 3D, photo), le texte exact et sa place. Ne donne pas ton avis,
+ décris.
+
+
+
+
+
Relisez le texte qu'elle a lu
+
+ Lors de nos essais, « boulangerie de quartier » est devenu « boutonnerie de
+ quartier ». Le texte exact, c'est le client qui le confirme.
+
+
+
+
+
+
+
+
+
+
+
+ Objectif : un logo imprimable, pas une jolie image.
+
+
+ Contexte : il servira sur [une étiquette, un autocollant
+ de 1,2 m, une carte].
+
+
+ Contraintes : style plat, 2 à 4 couleurs franches (les
+ codes relevés), aucun dégradé, aucune ombre, bords nets, fond blanc uni.
+
+
+ Format : un fichier SVG, aux dimensions finales.
+
+
+ Critères : chaque forme se distingue nettement de sa
+ voisine.
+
+
+
+
+
+ Demandez-le « dans un bloc de code » : un bouton Copier apparaît. Collez-le
+ dans un fichier texte, enregistrez en .svg, ouvrez-le dans votre logiciel :
+ net à 5 cm comme à 5 m.
+
+
+ Bien sur un logo simple, fait de formes et d'aplats. Une image regénérée
+ reste une image : 768 px, soit 6,5 cm à 300 ppp.
+
+
+
+
+
+
+
+
+
+ Un courriel de cinq lignes : ce qui n'allait pas, ce que vous proposez, la
+ question qui manque (« quelle police ? »).
+
+
+ VTracer et Potrace transforment une image en tracés vectoriels.
+
+
+ Upscayl et Real-ESRGAN reconstruisent les détails. Les brancher à votre outil
+ d'IA : séance 4.
+
+
+
+
+ {/* ── Avant/après en sous-groupes (brique 12, reformatée D116–D118) ── */}
+
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+
Sous-groupe {i}
+
Cas {i}
+
Tiré de votre inventaire S1 — à compléter.
+
+ ))}
+
+
+
+
+
+
+
+
+ {SIX.map(([c, q]) => (
+
+ ))}
+
+
+
+ {/* ── Ancrer : le Promptuaire ───────────────────────────── */}
+
+
+ Chaque prompt « après » entre au{' '}
+ Promptuaire
+ >
+ }
+ center
+ />
+
+ Sous la tâche métier de l'inventaire.
+
+ Noms de clients remplacés par des crochets, pour qu'un collègue le reprenne.
+
+ Ce qu'il produit.
+
+
+ Avant de quitter : trois questions, deux minutes, sur la plateforme.
+
+
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
« … »
+
+ — prénom, cas {i}
+
+
+ ))}
+
+
+
+ {/* ── Pont vers la séance 3 (briques 18, 19) ────────────── */}
+
+
+ Et si le modèle connaissait{' '}
+ vos documents ?
+ >
+ }
+ sub="À chaque fois, il faut tout réexpliquer : vos gabarits, vos façons de faire. À la séance 3, on lui donne la matière."
+ center
+ />
+
+
+ Une seule chose à faire d'ici là :{' '}
+ choisir les documents que vous nous envoyez .
+
+
+
+
+
+
+
+
+ Dans le doute sur le niveau : on n'envoie pas, on pose la question.
+
+
+
+
+
+ Votre matière, pour la séance 3
+ >
+ }
+ center
+ />
+
+
+ 1 ou 2 documents que vous touchez chaque semaine : gabarit de soumission,
+ procédure, guide de normes. PDF, Word, Excel — pas de PDF scanné ni de .indd.
+
+
+ Par courriel, à l'adresse du courriel de fin de séance.{' '}
+ Une semaine avant la séance 3.
+
+
+ On refait le tri à la réception et on réécrit les exercices sur votre
+ matière. Vos documents restent dans votre cohorte.
+
+
+
+ Merci — à la séance 3.
+
+
+
+ );
+}
diff --git a/src/styles/tokens.css b/src/styles/tokens.css
index a46a563..fcb1e5b 100644
--- a/src/styles/tokens.css
+++ b/src/styles/tokens.css
@@ -2,35 +2,29 @@
THEME TOKENS — edit ONLY this :root block to re-theme the whole deck.
The SAME token vocabulary as the `slides` (Slidev) and `webdeck` skills,
so a brand looks identical across all of them. The values below are a
- "dark product" default — REPLACE them to match the brief or the brand.
+ sober dark slate with one muted blue accent (Zoom screen-share readability).
=================================================================== */
:root {
/* Surfaces */
- --bg: #05070a;
- --bg-grad-1: rgba(79, 229, 176, 0.12);
- --bg-grad-2: rgba(41, 192, 240, 0.08);
+ --bg: #0f141b;
+ --bg-grad-1: rgba(120, 160, 230, 0.07);
+ --bg-grad-2: rgba(120, 160, 230, 0.04);
--surface: rgba(255, 255, 255, 0.045);
--surface-2: rgba(255, 255, 255, 0.07);
/* Ink */
- --fg: #f4f7fa;
- --fg-muted: #9aa4b2;
- --fg-faint: #626d7d;
+ --fg: #f2f4f7;
+ --fg-muted: #b4bcc8;
+ --fg-faint: #7d8796;
/* Hairlines */
--hair: rgba(255, 255, 255, 0.1);
--hair-2: rgba(255, 255, 255, 0.06);
/* Accent — ONE accent. May be a 2–4 stop gradient within ~80° of hue. */
- --primary: #4fe5b0;
- --accent: linear-gradient(
- 115deg,
- #c8f56e 0%,
- #5aeeb0 42%,
- #34dcd8 74%,
- #29c0f0 100%
- );
- --accent-ink: #04140e;
+ --primary: #8db4f2;
+ --accent: linear-gradient(115deg, #a9c8f7 0%, #8db4f2 60%, #7aa2e8 100%);
+ --accent-ink: #0b1220;
/* Radius */
--radius: 22px;
@@ -39,7 +33,7 @@
/* Depth */
--shadow: 0 50px 120px -50px rgba(0, 0, 0, 0.9);
- --glow: 0 30px 80px -40px rgba(79, 229, 176, 0.5);
+ --glow: 0 30px 80px -40px rgba(141, 180, 242, 0.35);
/* Type */
--font-head: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display',
@@ -54,7 +48,7 @@
--ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
/* Atmosphere */
- --grain: 0.035;
+ --grain: 0.02;
--vignette: rgba(0, 0, 0, 0.22);
/* Responsive gutter — fluid side / vertical padding */
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/vite.config.ts b/vite.config.ts
index 0466183..1c816c0 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,45 @@
import { defineConfig } from 'vite';
+import type { Plugin } from 'vite';
import react from '@vitejs/plugin-react';
+import fs from 'node:fs';
+import path from 'node:path';
+
+/* Dev-only slide review (R): GET/POST /__review?deck= reads/writes
+ src/decks//revue.json, versioned next to the deck it reviews. */
+function review(): Plugin {
+ return {
+ name: 'deck-review',
+ apply: 'serve',
+ configureServer(server) {
+ server.middlewares.use('/__review', (req, res) => {
+ const deck = new URL(req.url ?? '', 'http://x').searchParams.get('deck') ?? '';
+ const dir = path.resolve(__dirname, 'src/decks', deck);
+ if (!/^[a-z0-9-]+$/.test(deck) || !fs.existsSync(dir)) {
+ res.statusCode = 400;
+ return res.end('unknown deck');
+ }
+ const file = path.join(dir, 'revue.json');
+ if (req.method === 'POST') {
+ let body = '';
+ req.on('data', (c) => (body += c));
+ req.on('end', () => {
+ try {
+ fs.writeFileSync(file, JSON.stringify(JSON.parse(body), null, 2) + '\n');
+ res.end('ok');
+ } catch {
+ res.statusCode = 400;
+ res.end('invalid json');
+ }
+ });
+ return;
+ }
+ res.setHeader('Content-Type', 'application/json');
+ res.end(fs.existsSync(file) ? fs.readFileSync(file) : '{}');
+ });
+ },
+ };
+}
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), review()],
});